Skip to content

About

The Blocknet mining pool behind bntpool.com: Stratum server, PPLNS payouts and a live web dashboard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

772 Commits

Folders and files

Repository files navigation

Blocknet Pool (Rust)

Blocknet mining pool server in Rust (stratum + API + payouts).

Status Rust License

Windows setup guide · Connect to bntpool.com · Mining locally but pool hashrate is zero?

Runtime Modes

The repo ships four runtime binaries from a Cargo workspace:

  • blocknet-pool-api: API/UI process only
  • blocknet-pool-stratum: Stratum + payouts + maintenance process only
  • blocknet-pool-monitor: on-host monitoring sampler + Prometheus endpoint
  • blocknet-pool-recoveryd: privileged local recovery agent for daemon cutover/sync/rebuild workflows

For production, prefer the split services so API/UI deploys do not drop Stratum connections.

Workspace layout:

  • apps/: service-specific app packages
  • crates/: shared library packages with explicit dependency boundaries
  • frontend/: embedded React/Vite UI for the API binary

Payout operations

Exchange payout memos are opt-in, with separate accounting and a configurable higher payout minimum. New memo mining is disabled by default.

The payout minimum and regular interval remain configurable; this change does not alter deployed settings. Sweeps honor the configured recipient and amount caps, with at most 64 transactions and a three-minute budget checked between sends. Capacity or temporary send failures can schedule a five-minute continuation; regular sweep times stay anchored instead of drifting by processing time. Unknown broadcast outcomes retain their existing idempotency state and are reconciled before retrying. Operator recipient/amount caps do not themselves trigger a continuation.

Wallet inventories with at least 128 outputs worth 0.01 BNT or more are marked fragmented. Ordinary single-input payouts may include up to three additional small inputs, within the existing reserve budget. Dust is excluded, and fragmented wallets are not split further. Planning uses complete recent payout transactions. The daemon's optional GET /api/wallet/outputs?status=unspent filter reduces output transfer; older daemons that ignore the query remain compatible.

The persisted runtime payouts snapshot includes queued_recipient_count, continuation_required, blocked_address_count, and blocked_address_amount (atomic units). Blocked balances remain credited and are excluded from sends. For a read-only mainnet report, run python3 scripts/audit_payout_balances.py; use --include-addresses only when an operator needs the destinations. The script uses SSH, local PostgreSQL read-only queries, and libsodium key validation. Invalid-address claims require ownership review and a validated replacement address before a separately authorized ledger correction. Never silently redirect, erase, or retry those funds against an invalid key.

Runtime tests

Database-backed tests require BLOCKNET_POOL_TEST_POSTGRES_URL; without it, those tests return early without exercising PostgreSQL. Use a disposable local PostgreSQL database with permission to create schemas. Each test creates its own schema so the suite can run concurrently; discard the database afterward to remove the test schemas.

BLOCKNET_POOL_TEST_POSTGRES_URL='host=localhost user=postgres dbname=pool_test' \
  cargo test -p pool-runtime -- --test-threads=4
cargo clippy -p pool-runtime -p blocknet-pool-stratum-app --all-targets -- -D warnings

The replay tests exercise the real Argon2 implementation and require several GiB of available memory.

Deploy (bntpool)

From the local repo root:

./scripts/deploy_bntpool.sh

What it does:

  • builds frontend bundle locally (unless --skip-ui-build)
  • builds the split release binaries on Linux:
    • set BNTPOOL_REMOTE_BUILD=1 to build on bntpool after source sync
    • or set BNTPOOL_LOCAL_BUILD_IMAGE with DOCKER_DEFAULT_PLATFORM=linux/amd64 to build locally in Docker
  • rsyncs pool source to bntpool:/opt/blocknet/blocknet-pool
  • uploads locally built binaries, or uses host-built binaries when BNTPOOL_REMOTE_BUILD=1
  • verifies deployed binary architecture before restarting services
  • writes /opt/blocknet/blocknet-pool/deploy-info.txt
  • restarts only the changed service(s):
    • blocknet-pool-api.service
    • blocknet-pool-stratum.service
    • blocknet-pool-monitor.service
  • frontend-only changes still restart blocknet-pool-api.service because the UI bundle is embedded into that binary
  • tails recent logs and runs scripts/verify_bntpool_deploy.sh

From macOS, do not run a plain local binary deploy. Use the remote build path:

BNTPOOL_REMOTE_BUILD=1 ./scripts/deploy_bntpool.sh

Run the production smoke verifier without deploying:

./scripts/verify_bntpool_deploy.sh

Monitoring Stack

The pool now ships a dedicated on-host monitor and repo-managed monitoring assets:

  • blocknet-pool-monitor.service: probes API, Stratum, Postgres, daemon, wallet, share freshness, and payout queue state every 10s
  • The monitor uses the same api_key as protected API routes when it samples API performance telemetry.
  • DB-backed heartbeats and incidents drive /api/status
  • Prometheus + Alertmanager + node exporter + blackbox exporter configs live under deploy/monitoring/
  • Cloudflare Worker assets for outside-in public HTTP probes live under deploy/cloudflare/monitor-worker/

Provision the on-host monitoring packages and configs on bntpool with:

./scripts/provision_bntpool_monitoring.sh

That script installs Prometheus, Alertmanager, node exporter, blackbox exporter, and the local Discord relay. Grafana provisioning files are included in-repo and can be installed separately if Grafana is present on the host.

Growth mode is an informational operating state in Prometheus and Alertmanager; it does not send Discord notifications. Capacity, verification coverage, backlog and service-health alerts continue to notify Discord normally.

Template renewal errors remain counted, but notify only while template recovery is overdue. A recent successful template fetch or renewal suppresses the historical error alerts immediately. Unknown template IDs can follow a daemon restart or switch before their leases expire; they become critical only when recovery stays overdue. Missing freshness telemetry does not suppress a recorded failure. General refresh-stall and block-submission-failure alerts remain independent.

Validate the rules and recovery scenarios before provisioning:

cd deploy/monitoring/prometheus
promtool check rules blocknet-pool.rules.yml
promtool test rules template-lease.test.yml

Build Blocknet Daemon For bntpool

When you need a fresh blocknet-core daemon binary for the pool host, build it locally from this repo using the sibling daemon checkout instead of compiling on bntpool. The build and deploy scripts source the daemon pool branch from the sibling ../blocknet-core repo through a temporary git worktree, so they do not require switching your current local checkout first. If that branch/ref is missing or stale locally, fetch it before building.

From blocknet-pool/:

./scripts/build_blocknet_daemon.sh

That writes the daemon artifact to build/blocknet-core-linux-amd64.

To stage it on the server without restarting the daemon yet:

./scripts/build_blocknet_daemon.sh --upload bntpool

For the full repeatable deploy path, including the managed blocknetd@primary and blocknetd@standby units, release directory rotation, symlink switch, rolling restarts, and wallet/mining readiness verification:

./scripts/deploy_blocknet_daemon_bntpool.sh

blocknetd@primary is also the public seed node seed.bntpool.com: it runs with --seed, so it serves its peer ID on TCP 28081 next to P2P on 28080 (both open in ufw). Its peer ID comes from /var/lib/blocknet/data/identity.key; the seed list depends on that file, so back it up and never delete it. The standby listens for P2P on 28090, which stays closed, so it never takes the seed ports.

The deploy upgrades the currently inactive instance first. It waits for its wallet to load and catch up, agreement with the serving daemon's canonical chain, and a successful template fetch and renewal. Recoveryd must already be configured with the wallet secret and managed proxy/cookie routing. The helper also needs python3 and psql on the host and a fresh Stratum runtime snapshot in Postgres.

Before switching traffic, it pauses payouts and waits for a payout tick completed after the pause, so an existing batch can finish. It uses recoveryd's transactional proxy/cookie cutover, verifies the pool has refreshed its template, and only then restarts the original active instance. It returns traffic to that original wallet before restoring the previous payout pause state, preserving its unconfirmed-send state. API and Stratum services are not restarted. An old instance-local template can still produce a renewal alert during cutover; these alerts are not suppressed. A block found while the daemon is unreachable is resubmitted for up to five minutes, falling back to the full block when the daemon no longer knows the template, so a cutover or daemon restart does not lose it (blocknet_pool_monitor_block_submit_retries_total, blocknet_pool_monitor_block_submit_recovered_total).

Each readiness/drain gate defaults to 900 seconds, configurable with BNTPOOL_DAEMON_READY_TIMEOUT. A failure before cutover leaves the original daemon serving and restores the previous release pointer. A failure once cutover starts leaves payouts paused and stops further restarts; inspect routing and wallet readiness and return to the original wallet before resuming payouts.

Check the serving daemon, routing, recovery service, and runtime snapshot without deploying or changing service state:

ssh bntpool 'sudo -n python3 - --check' < scripts/rolling_daemon_deploy.py
python3 -m unittest discover -s scripts -p 'test_rolling_daemon_deploy.py'

By default the build script reads source from ../blocknet-core, derives the required Go version from that repo's go.mod, resolves branch pool from the daemon repo, builds through the daemon repo's Dockerfile, writes local artifact metadata next to the binary, and uploads to /opt/blocknet/blocknet-core/blocknet.new. The full deploy script names releases with the branch prefix, for example pool-<sha>, and records build-info.txt in each release directory on the server.

Local Development

npm --prefix frontend ci
npm --prefix frontend run build
cargo build --release -p blocknet-pool-api-app --bin blocknet-pool-api
cargo build --release -p blocknet-pool-stratum-app --bin blocknet-pool-stratum
cargo build --release -p blocknet-pool-monitor-app --bin blocknet-pool-monitor
cargo build --release -p blocknet-pool-recoveryd-app --bin blocknet-pool-recoveryd
cp config.example.json config.json
# edit config.json and set database_url, api_key, monitor_ingest_secret, daemon_cookie_path, and pool addresses
# optional: create .env next to config.json with BLOCKNET_WALLET_PASSWORD=...
cargo run --release -p blocknet-pool-api-app --bin blocknet-pool-api
cargo run --release -p blocknet-pool-stratum-app --bin blocknet-pool-stratum
cargo run --release -p blocknet-pool-monitor-app --bin blocknet-pool-monitor
# run the API and Stratum binaries in separate terminals
# run the monitor binary in a third terminal if you want the DB-backed status page/metrics loop

Custom config:

cargo run --release -p blocknet-pool-api-app --bin blocknet-pool-api -- --config /path/to/config.json
cargo run --release -p blocknet-pool-stratum-app --bin blocknet-pool-stratum -- --config /path/to/config.json
cargo run --release -p blocknet-pool-monitor-app --bin blocknet-pool-monitor -- --config /path/to/config.json
cargo run --release -p blocknet-pool-recoveryd-app --bin blocknet-pool-recoveryd -- --config /path/to/config.json

Frontend

Web UI is built with React + TypeScript + Vite from frontend/.

Local dev:

cd frontend
npm install
npm run dev

Build the embedded API bundle:

cd frontend
npm ci
npm run build

Build output is written to frontend/dist/ and embedded into the API binary during API builds. A fresh clone must build the frontend before cargo build or cargo run for blocknet-pool-api.

The embedded assets are served at:

  • GET / (index)
  • GET /ui-assets/app.js
  • GET /ui-assets/app.css

Because the UI bundle is embedded into the API binary, frontend-only changes still require rebuilding and restarting the API service.

Daemon API Auth

The pool authenticates to the daemon with daemon_cookie_path in config.json. When a daemon request returns 401 unauthorized, the pool refreshes the token from the configured cookie path once and retries.

Database Backend

The pool uses Postgres. Set database_url in config.json, for example:

{
  "database_url": "postgres://user:password@127.0.0.1:5432/blocknet_pool"
}

database_url is required. database_pool_size controls Postgres connection fan-out (default 4).

Transport Security

  • Stratum now defaults to loopback bind (stratum_host=127.0.0.1). Set stratum_host explicitly to expose it.
  • The API server is plaintext HTTP and should stay bound to loopback behind nginx or another TLS terminator.
  • If Stratum is exposed publicly, place it behind a TLS terminator.

Runtime Components

  • API/UI server
  • Stratum server
  • Template/job manager
  • Validation engine (bounded queues)
  • Persistent storage (Postgres)
  • Payout processor
  • DB/meta-backed live runtime snapshot for split-service API status

API Surface

Public endpoints (no API key required):

  • GET /api/info
  • GET /api/checkpoints
  • GET /checkpoints.dat
  • GET /api/stats
  • GET /api/stats/history
  • GET /api/stats/insights
  • GET /api/luck
  • GET /api/status
  • GET /api/blocks
  • GET /api/payouts/recent
  • GET /api/miner/{address}
  • GET /api/miner/{address}/balance
  • GET /api/miner/{address}/hashrate

/checkpoints.dat serves the latest daemon-generated checkpoint file. By default the API reads <daemon_data_dir>/checkpoints.dat; set checkpoints_path in config.json to override that path.

Protected endpoints (API key required):

  • GET /api/miners
  • GET /api/health
  • GET /api/daemon/logs/stream

api_key is required at API and monitor startup. monitor_ingest_secret is required for the signed Cloudflare monitor ingest endpoint.

Accepted header:

  • x-api-key: <api_key>

Paged/filterable list endpoints:

  • Shared query params: limit, offset.
  • GET /api/miners: search, sort.
  • GET /api/blocks: status.

Daemon log stream details:

  • Admin UI includes a live daemon log viewer tab.
  • Stream endpoint: GET /api/daemon/logs/stream?tail=200.
  • Log source fallback order:
    • journalctl for the active recovery daemon, then the managed primary and standby units
    • tail -F <daemon_data_dir>/debug.log

Web UI

  • GET / (front page): the pool fee, Stratum address and a miner lookup for first visits; once a miner has been looked up, the hero shows that miner's hashrate and progress to its own payout minimum (exchange deposits use the higher memo minimum). Current Round lives on /luck.
  • Multi-tab WebUI includes:
    • pool + onboarding info (/api/info)
    • API key auth UX for protected routes
    • miner lookup
    • public blocks, payouts, luck, and status pages
    • admin miners, balances, shares, rewards, recovery, and logs tabs
    • live trend charts
    • operator health panel (/api/health)
    • live daemon logs panel (/api/daemon/logs/stream)

Stratum Notes

  • Login requires protocol v2 with declared capabilities.
  • The login address also accepts Blocknet ID shortnames (@name or $name, case-insensitive). Stratum verifies the signed mapping from https://blocknet.id, then applies normal address/network validation. Jobs, shares, balances, and payouts use the resolved full address, fixed for that connection. A new login resolves the name again; a name change never redirects an existing balance. Resolver failures reject login. Full addresses require no resolver access. The stats page also supports shortname lookups.
  • Login rejects malformed payout addresses early (base58 + checksum-compatible Blocknet stealth address validation)
  • Login requires protocol v2 + submit_claimed_hash; submits without claimed_hash are rejected.
  • Per-connection submit rate limiting is enabled.
  • refill (capability nonce_refill_v1, advertised at login, never required) gives a connection another nonce range for a job it holds, without reconnecting. Request {"id":N,"method":"refill","params":{"job_id":"<held job>"}}; the answer is {"id":N,"status":"ok","result":{...}} with the same fields as a job notification: a new job_id and a fresh, non-overlapping nonce range for the same template and share target. No job notification is sent for it. The connection's other jobs stay valid; a new job for the same template (vardiff or rebind) supersedes the job and all its refills together with the usual grace, and a new template retires them all with stale_submit_grace. Errors: stale job (not a live job of this connection on the current template), refill rate limited (stratum_refill_rate per second, burst stratum_refill_burst; defaults 4 and 8), refill limit reached (stratum_refill_max_assignments live jobs per connection per template, default 256, counting the job itself, or a pool-wide bound on refills), and not logged in before login.
  • proxy_workers_v1 (advertised at login, never required): a connection that logs in with it may add "worker":"<name>" to a submit, and that share is credited to that worker instead of the connection's own, so a farm proxy's rigs show up individually. Names are cleaned like login worker names, and each connection may use up to 4,096 distinct names; further names count as the connection's worker.
  • Queue pressure returns server busy, retry (no inline bypass)
  • Per-connection vardiff retargeting is enabled by default to target a small number of shares per window (vardiff_* config keys)
  • Vardiff difficulty is cached per address+worker and reused on reconnect/restart when the hint is fresh (1h TTL), reducing post-restart ramp-up.
  • Default vardiff profile assumes a weak baseline miner and aims for ~10 shares / 5 minutes (initial_share_difficulty=60, vardiff_target_shares=10)
  • Template refresh identity uses stable tip fields (height, network_target, prev_hash) to avoid daemon template churn while still refreshing on meaningful tip/template transitions.
  • Assignment submits on a previous template are accepted only inside a short grace window (stale_submit_grace, default 8s) based on when the share was received.
  • Daemon SSE tip events mark templates stale from new_block hash + height.
  • Timestamp-only new_block changes do not trigger refreshes; only hash/height changes can trigger staleness.
  • Same-height hash-change refresh is enabled by default (refresh_on_same_height=true) so the pool reacts immediately to same-height reorgs. Set it to false only if an operator intentionally wants to coalesce those events.
  • Daemons that advertise template_expires_at_unix_ms receive a cheap liveness renewal every 30 seconds (and never later than half-life) without replacing miner work. A rejected or malformed renewal queues a forced template replacement.
  • Generation-aware daemons attach mempool_generation to status and block-template responses. A same-tip generation change is debounced for 10 seconds and then forces one content refresh so newly arrived transactions enter miner work without restoring constant template churn.
  • Every mining template has a hard two-minute content age cap, even when the mempool is unchanged. This bounds on-chain timestamp drift while preserving renewable leases and avoiding the old two-second refresh churn.
  • A successful SSE connection/reconnect forces a same-tip template rotation because daemon template IDs are process-local. Against older daemons without renewal support, the pool bounds polling-only restart detection with the same 30-second rotation cadence.

Solo Mining

A miner selects solo mode per connection by logging in as solo:<address>, solo:@name or solo:$name (prefix in any letter case). Everything else about the connection is unchanged: same port, gateway, vardiff, verification, memos and payouts.

  • Solo shares are stored with shares.solo = true and never enter a PPLNS window, a pending-reward preview or a PPLNS trust count. In the minute projections they land in the solo and solo_rejected status buckets, so hashrate and pool-wide luck still count solo work while PPLNS readers skip it.
  • A block found by a solo connection is stored with blocks.solo = true. Once it confirms, its whole reward less pool_fee_pct is credited to the finder, then paid through the normal payout path. PPLNS miners earn nothing from it.
  • A solo connection's jobs are assigned to the owner solo:<address>, so work issued to one mode is rejected (job not assigned to this miner) if submitted through the other, even from the same address.
  • After a solo login the pool sends a solo_mining notification that explains the payout and the pool fee. Solo connections do not receive pool_block_solved broadcasts, and a solo block is not announced to PPLNS miners.
  • Pool hashrate, round effort and luck are pool-wide: they include solo work, and solo blocks end a round like any other block.
  • The API and stats page accept solo: lookups and show the miner's address. GET /api/blocks items and GET /api/luck rounds carry solo, GET /api/miner/{address} adds solo_hashrate and a solo flag on each worker (any solo share in the hashrate window), and pending-estimate block rows carry solo. The blocks and luck tables show a Type column (Pool or Solo, with an icon) before Status, and the front page's recent blocks show the same marker; a miner with solo work also gets it in pending estimates and the Workers table.
  • Get Started has a "Mine solo" section with the solo: command, shown only when the latest seine release (latest_miner_version in GET /api/info) is v0.3.1 or newer, because older seine versions reject the prefix. The rewards card explains the Solo type.

Payout Safeguards

  • Payout batches target at least 15 recipients when enough miners are eligible and wallet liquidity permits. Smaller queues still pay on the scheduled sweep; they do not wait for a full batch.
  • payout_interval defaults to 1h and min_payout_amount defaults to 0.1 BNT. A longer interval combines more earnings per miner into each payment, reducing repeat payments at the cost of a longer wait. Existing production configuration overrides these defaults and is preserved by the deploy script.
  • payouts_enabled toggles payout sending globally.
  • payout_pause_file pauses payouts when the file exists.
  • pplns_window_duration controls the time-based PPLNS lookback window (default 6h).
  • payout_provisional_cap_multiplier caps aged provisional difficulty relative to verified difficulty so undersampled miners receive reduced provisional credit instead of being fully excluded.
  • Keep sample_rate and min_sample_every comfortably above the payout policy's full-credit target so honest miners do not sit under the cap for long.
  • verification_policy (ratio by default) selects how verification is spent. count gives each large address about count_target_samples samples per count_window (never below count_coverage_floor) and also credits provisional shares that have a clean sample within count_sample_radius, once the window holds count_payout_min_samples. See VERIFY_COUNT_SAMPLING.md. The policy, count_payout_min_samples and count_sample_radius need a restart.
  • Optional caps:
    • payout_max_recipients_per_tick
    • payout_max_total_per_tick
    • payout_max_per_recipient

Retention & Summary Rollups

  • Retention worker runs hourly, keeping detailed shares for 90 days and payout rows for 365 days.
  • Old rows are rolled up into summary tables before prune:
    • share_daily_summaries
    • payout_daily_summaries
  • get_total_share_count and total rejected share metrics include rolled-up share summaries.

CI Coverage

  • Added CI smoke harness: scripts/ci_e2e_smoke.sh (mock daemon + real pool process + Stratum/API probe).

Daemon Requirements

The pool expects a running Blocknet daemon API with mining + wallet routes enabled.

At minimum:

  • GET /api/status
  • GET /api/mining/blocktemplate
  • POST /api/mining/submitblock
  • GET /api/block/{id}
  • GET /api/wallet/address
  • GET /api/wallet/balance
  • POST /api/wallet/send
  • POST /api/wallet/load
  • POST /api/wallet/unlock

Notes

  • This repository no longer uses Go; runtime is Rust-only.

Miner release notices

The API checks the latest stable release of zpalmtree/seine on GitHub at startup and every 15 minutes. Miner requests use the cached version, so GitHub latency never delays a lookup. Failed checks retry after one minute and keep the last successful version in memory. On a fresh start, update notices stay hidden until a check succeeds. The optional latest_miner_version config field supplies a startup fallback; leave it empty for automatic discovery without a fixed fallback. Drafts, prereleases, malformed tags and failed HTTP responses cannot replace the cached release. Existing miner response caches may take another 10 seconds to expire after an update.

About

The Blocknet mining pool behind bntpool.com: Stratum server, PPLNS payouts and a live web dashboard.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages