A production-ready, low-latency solo mining pool for DigiByte with zero dependencies.
High-priority goals for this build:
- Low latency to your local DigiByte node (LAN-adjacent or same host)
- Simple self-hosting with Coolify (
Dockerfile+ compose) - Solo focused payout model (single payout address)
- Multiple miners/devices over Stratum V1
| Feature | Status |
|---|---|
| Stratum V1 Protocol | ✅ Full implementation with version rolling |
| Version-Mask Slicing | ✅ Per-miner disjoint ASICBoost mask assignment with auto-fallback |
| ESP-Miner Notify Coalescing | ✅ Adaptive non-clean notify pacing to reduce queue churn |
set_extranonce Orchestration |
✅ Extranonce namespace rotation before template churn (when supported) |
| Two-Stage New-Block Fastpath | ✅ Near-empty first notify, then full template follow-up |
| SHA256d Mining | ✅ Native validation, other algos planned |
| Variable Difficulty | ✅ Per-miner automatic retargeting |
ZMQ hashblock Detection |
✅ Sub-millisecond new block push notifications |
| Longpoll (Optional Fallback) | ✅ Supported when ZMQ is disabled |
| Rate Limiting | ✅ DoS protection with per-IP limits |
| Live Dashboard | ✅ Real-time stats with confetti on blocks 🎉 |
| Prometheus Metrics | ✅ Full observability with latency histograms |
| TLS/HTTPS | ✅ Optional secure API endpoints |
| CORS Support | ✅ Configurable cross-origin access |
| Docker Security | ✅ Non-root user, minimal attack surface |
| Zero Dependencies | ✅ Pure Node.js built-in modules only |
- What it is
- Current scope / performance notes
- Quick start
- Miner connection
- DigiByte node config tips
- Endpoints
- Security & Rate Limiting
- API Server Configuration
- Prometheus Metrics
- Development
- Important compatibility note
- Vardiff
- Environment Variables Reference
- Next performance upgrades
- Troubleshooting
- Contributing
- Raw TCP Stratum server (
mining.subscribe,authorize,notify,submit) mining.configuresupport for version-rolling (overt ASICBoost) negotiation- Per-miner version-rolling mask slicing with reject-streak auto-fallback to full mask and auto-disable when <2 compatible miners
- ESP-Miner-aware non-clean notify coalescing with adaptive pacing based on latency/reject signals
- Extranonce namespace orchestration via
mining.set_extranonce(when miner subscribed) - Direct DigiByte JSON-RPC
getblocktemplate+submitblock - ZMQ
hashblocksubscription support for fastest new-block propagation - Longpoll support as a fallback signal channel
- Two-stage new-block propagation: fast near-empty template, then full template
- Per-connection extranonce1 partitioning (prevents active miner nonce-space overlap)
- In-memory solo pool state (no DB, no web framework overhead)
- Real-time dashboard via Server-Sent Events (zero dependencies)
- Status endpoints:
/healthz,/stats,/events,/metrics - Connection rate limiting and DoS protection
- Optional TLS/HTTPS for API endpoints
- Comprehensive Prometheus metrics with histograms
- Live dashboard with real-time updates and confetti celebrations
POW_ALGO=sha256donly (validated locally with built-in Node crypto)- Optimized for low software overhead on a LAN and direct node adjacency
- Actual end-to-end latency depends mostly on:
- miner firmware behavior
- node responsiveness
- LAN quality / switch buffering
- ASIC TCP stack quirks
This implementation is designed to minimize pool-side overhead, but it cannot guarantee "beating" every hosted pool in every scenario because WAN routing and miner firmware dominate some latencies.
- Create
.envfrom.env.example. - Set
POOL_PAYOUT_ADDRESSto your DigiByte address (required). - Ensure DigiByte Core RPC is reachable from this host/container.
- Start with Docker/Coolify or local Node.
Important:
- Do not commit your local
.env(RPC credentials, payout address, and auth tokens are secrets). .env.exampleis a template only.
node src/index.jscp .env.example .env
docker compose -f docker-compose.coolify.yml up -d --buildCoolify can import this repo and deploy either:
Dockerfilemode (recommended)docker-compose.coolify.ymlmode
Point miners to:
stratum+tcp://<pool-host>:3333
Worker/user can be any string by default (ALLOW_ANY_USER=true), for example:
- user:
rig01 - pass:
x,d=16384
If you set MINER_AUTH_TOKEN, include it in password:
x,token=YOURTOKEN,d=32768
Your node should have RPC enabled and reachable by the pool container.
Typical requirements:
server=1algo=sha256d(required for this pool)rpcuser=...rpcpassword=...rpcallowip=<coolify/docker network>rpcbind=0.0.0.0(or specific LAN IP)
For ZMQ new-block push notifications (recommended):
zmqpubhashblock=tcp://0.0.0.0:28332(or a specific LAN IP)- Allow pool host/container to reach that endpoint
Important on DigiByte multi-algo setups:
- This pool validates
sha256donly. - The pool now checks
getmininginfo.pow_algoat startup and exits if the node reports a different algo (for examplescrypt). - Point
NODE_RPC_*at a DigiByte daemon instance configured withalgo=sha256d.
GET /or/dashboard- Live web dashboard with real-time updatesGET /events- Server-Sent Events (SSE) stream for real-time statsGET /healthz- Health check (returns 200 if pool has active job)GET /stats- JSON stats snapshot (connections, shares, blocks, workers)GET /metrics- Prometheus metrics (plaintext format)
The pool includes built-in DoS protection and connection management:
# Maximum total concurrent connections
MAX_CLIENTS=1000
# Maximum connections per IP address
MAX_CLIENTS_PER_IP=10
# Maximum connection attempts per IP per minute
CONNECTION_RATE_LIMIT_PER_MIN=60How it works:
- New connections are checked against total client limit
- Per-IP limits prevent single source from consuming all slots
- Rate limiting uses sliding 60-second window
- Rejected connections are logged with reason
Recommended settings:
- Solo mining (trusted IPs): Set high limits or disable (
MAX_CLIENTS=10000) - Public pool: Use defaults or lower (
MAX_CLIENTS_PER_IP=5) - Behind reverse proxy: Consider using proxy IP header (requires code modification)
The pool runs as a non-root user (pooluser, UID 1001) in Docker for defense-in-depth security.
Enable browser-based dashboard access from different domains:
# Enable CORS headers (default: true)
API_CORS_ENABLED=true
# Allowed origin (default: *, use specific domain in production)
API_CORS_ORIGIN=*
# Or restrict to specific domain:
# API_CORS_ORIGIN=https://pool.example.comSecure your API and dashboard with HTTPS:
# Enable TLS (default: false)
API_TLS=true
# Paths to certificate and key (required if API_TLS=true)
API_TLS_CERT=/path/to/cert.pem
API_TLS_KEY=/path/to/key.pemGenerate self-signed certificate for testing:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodesProduction deployment:
- Use Let's Encrypt certificates via certbot
- Place cert files in persistent volume
- Update
.envwith correct paths - Ensure certificate renewal process updates the pool
Note: Stratum (port 3333) is always TCP. Only HTTP API supports TLS.
The pool exports comprehensive metrics at /metrics in Prometheus format:
pool_connected_clients # Current connected clients
pool_authorized_clients # Current authorized clients
pool_templates_fetched_total # Total templates fetched from node
pool_job_broadcasts_total # Total job broadcasts to miners
pool_shares_accepted_total # Total accepted shares
pool_shares_rejected_total # Total rejected shares
pool_shares_stale_total # Total stale shares
pool_shares_duplicate_total # Total duplicate shares
pool_shares_lowdiff_total # Total low-difficulty shares
pool_blocks_found_total # Total blocks found
pool_blocks_rejected_total # Total blocks rejected by node
pool_current_height # Current blockchain height
pool_uptime_seconds # Pool uptime in seconds
api_requests_total # Total API requests
api_requests_by_path{path="/stats"} # Requests by endpoint
api_request_duration_seconds_bucket{le="0.01"} # Latency histogram
api_request_duration_seconds_count # Total latency samples
Latency histogram buckets: 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s, +Inf
Example Prometheus scrape config:
scrape_configs:
- job_name: 'digibyte-pool'
static_configs:
- targets: ['localhost:8080']
scrape_interval: 15sExample Grafana queries:
# Current hashrate estimate from share rate
rate(pool_shares_accepted_total[5m]) * BASE_DIFFICULTY * (2^32) / 600
# Share acceptance rate
rate(pool_shares_accepted_total[5m]) / rate(pool_shares_rejected_total[5m])
# API latency p95
histogram_quantile(0.95, rate(api_request_duration_seconds_bucket[5m]))
- Node.js >= 20
src/
├── index.js # Entry point, signal handling
├── config.js # Configuration loading and validation
├── logger.js # JSON structured logging
├── utils.js # Bitcoin/crypto primitives
├── rpc.js # DigiByte RPC client
├── job-manager.js # Block template and job management
├── stratum-server.js # Stratum V1 protocol server
├── api-server.js # HTTP/HTTPS API server
├── dashboard-page.js # Dashboard HTML/CSS/JS
├── coinbase-builder.js # Coinbase transaction construction
├── merkle.js # Merkle tree operations
└── share-validator.js # Share validation and header building
- Edit code
- Test locally with
node src/index.js - Check Docker build:
docker build -t dgb-pool .
Stratum prevhash formatting varies across miner implementations. Default is STRATUM_PREVHASH_MODE=stratum.
If a miner subscribes but never submits valid shares, test:
STRATUM_PREVHASH_MODE=stratum_wordrev(required for NerdOCTAXE BM1370 firmware in local testing)STRATUM_PREVHASH_MODE=headerSTRATUM_PREVHASH_MODE=rpc
The pool can auto-rotate prevhash modes after repeated lowdiff shares, but set the correct mode explicitly once known to avoid startup thrash.
Per-miner vardiff is enabled and retargets based on accepted share timing.
Useful knobs:
ENABLE_VARDIFF=true|falseVARDIFF_TARGET_SHARE_TIME_MS(default15000)VARDIFF_RETARGET_EVERY_SHARES(default4)VARDIFF_MAX_DIFFICULTY(defaultBASE_DIFFICULTY)
Complete list of configuration options (see .env.example):
NODE_RPC_HOST=127.0.0.1 # RPC host
NODE_RPC_PORT=14022 # RPC port (14022 = mainnet)
NODE_RPC_USER=your-rpc-user # RPC username
NODE_RPC_PASS=your-rpc-password # RPC password
NODE_RPC_TLS=false # Use HTTPS for RPC (rare)
NODE_RPC_TIMEOUT_MS=5000 # Normal RPC timeout
NODE_RPC_LONGPOLL_TIMEOUT_MS=90000 # Longpoll timeout
ENABLE_ZMQ_HASHBLOCK=true # Use ZMQ hashblock push notifications (default)
ZMQ_HASHBLOCK_ENDPOINT= # Optional override (default: tcp://NODE_RPC_HOST:28332)
ZMQ_RECONNECT_BASE_MS=250 # Initial reconnect delay
ZMQ_RECONNECT_MAX_MS=10000 # Max reconnect delay
ALLOW_NODE_POW_ALGO_MISMATCH=false # Allow algo mismatch (not recommended)STRATUM_HOST=0.0.0.0 # Bind address (0.0.0.0 = all interfaces)
STRATUM_PORT=3333 # Stratum port
STRATUM_PREVHASH_MODE=stratum # Prevhash format: stratum|stratum_wordrev|header|rpc
SOCKET_IDLE_TIMEOUT_MS=300000 # Idle socket timeout (5 minutes)MAX_CLIENTS=1000 # Maximum total connections
MAX_CLIENTS_PER_IP=10 # Maximum connections per IP
CONNECTION_RATE_LIMIT_PER_MIN=60 # Max connection attempts per IP per minuteAPI_HOST=0.0.0.0 # API bind address
API_PORT=8080 # API port
API_TLS=false # Enable HTTPS
API_TLS_CERT=/path/to/cert.pem # TLS certificate path (required if API_TLS=true)
API_TLS_KEY=/path/to/key.pem # TLS key path (required if API_TLS=true)
API_CORS_ENABLED=true # Enable CORS headers
API_CORS_ORIGIN=* # CORS allowed originPOW_ALGO=sha256d # Proof-of-work algorithm (sha256d only currently)
BASE_DIFFICULTY=16384 # Starting difficulty for new miners
MIN_DIFFICULTY=1 # Minimum allowed difficulty
ENABLE_VARDIFF=true # Enable variable difficulty
VARDIFF_TARGET_SHARE_TIME_MS=15000 # Target time between shares (15 seconds)
VARDIFF_RETARGET_EVERY_SHARES=4 # Retarget difficulty every N shares
VARDIFF_MAX_DIFFICULTY=16384 # Maximum allowed difficulty
EXTRANONCE1_SIZE=4 # Extranonce1 size in bytes (2-16)
EXTRANONCE2_SIZE=8 # Extranonce2 size in bytes (2-16)
VERSION_ROLLING_MASK=1fffe000 # Overt ASICBoost mask advertised via mining.configure
VERSION_ROLLING_MIN_BIT_COUNT=1 # Minimum rolling bits requested from miners
ENABLE_VERSION_MASK_SLICING=true # Per-miner disjoint version-mask slices (auto-active only with >=2 compatible miners)
DISABLE_SLICING_FOR_NERDAXE=true # Compatibility profile: keep NerdAxe/NerdOctaxe on full mask
VERSION_MASK_SLICE_BITS_PER_MINER=2 # Target number of rolling bits per miner slice
VERSION_MASK_SLICE_FALLBACK_REJECTS=8 # Auto-fallback to full mask after N reject streak
ENABLE_ESP_MINER_NOTIFY_COALESCING=true # Coalesce non-clean notifies for ESP-Miner class clients
ESP_MINER_NOTIFY_NONCLEAN_MIN_INTERVAL_MS=3000 # Minimum spacing for non-clean notify to ESP lane
ESP_MINER_NOTIFY_FORCE_INTERVAL_MS=15000 # Force a non-clean refresh if coalescing held too long
ENABLE_ADAPTIVE_NOTIFY_PACING=true # RTT/reject/share-rate driven non-clean notify pacing
ADAPTIVE_NOTIFY_BASE_INTERVAL_MS=400 # Baseline non-clean notify spacing
ADAPTIVE_NOTIFY_TARGET_SHARE_MS=2000 # Faster shares than this increase pacing interval
ADAPTIVE_NOTIFY_MAX_INTERVAL_MS=5000 # Cap on adaptive non-clean pacing
ADAPTIVE_NOTIFY_HIGH_ACK_MS=200 # Submit ack latency threshold for pacing backoff
ADAPTIVE_NOTIFY_HIGH_REJECT_RATIO_PCT=5 # Reject-rate threshold for pacing backoff
ENABLE_SET_EXTRANONCE_ORCHESTRATION=true # Rotate extranonce namespace (if supported) before template churn
SET_EXTRANONCE_ROTATE_COOLDOWN_MS=10000 # Min ms between set_extranonce rotations per minerPOOL_PAYOUT_ADDRESS=dgb1... # Your DigiByte payout address (REQUIRED)
POOL_PAYOUT_ADDRESS_EXPLORER_BASE=https://digiexplorer.info/address/ # Dashboard explorer base URL for payout address
POOL_PAYOUT_SCRIPT_HEX= # Advanced: Raw scriptPubKey hex (optional, leave empty)
POOL_TAG=/your-pool-tag/ # Pool identifier in coinbase (max ~20 chars)ALLOW_ANY_USER=true # Allow any username (true for solo mining)
MINER_AUTH_TOKEN= # Optional: Require token in password (e.g., token=SECRET)ENABLE_LONGPOLL=false # Enable longpoll signal channel (optional fallback)
TEMPLATE_POLL_MS=1000 # Template poll interval when no healthy signal channel
TEMPLATE_POLL_MS_LONGPOLL_HEALTHY=500 # Backup poll even when signal channel is healthy
LONGPOLL_HEALTHY_GRACE_MS=120000 # Healthy-signal grace window (ms)
ENABLE_NEW_BLOCK_FASTPATH=true # On ZMQ/longpoll new block, send minimal tx template first
NEW_BLOCK_FASTPATH_TX_LIMIT=0 # Tx count for fastpath template (0 = near-empty block)
ENABLE_SPECULATIVE_NEXT_TEMPLATE_PREBUILD=true # Prebuild N+1 coinbase/merkle artifacts in background
ENABLE_PROACTIVE_NONCE_SPACE_REFRESH=true # Proactively refresh active job nonce space for fast miners
NONCE_SPACE_REFRESH_FAST_SHARE_MS=2000 # Trigger refresh when miner avg share interval is at/under this
NONCE_SPACE_REFRESH_COOLDOWN_MS=8000 # Min ms between nonce-space refreshes per miner
NONCE_SPACE_REFRESH_DUPLICATE_STREAK=3 # Trigger refresh after N consecutive duplicate shares
TEMPLATE_FINGERPRINT_MODE=fast # Template change detection: fast|full|prevhash
KEEP_OLD_JOBS=8 # Number of recent jobs to keep in memory
MAX_JOB_SUBMISSIONS_TRACKED=50000 # Maximum dedupe entries per job
ENABLE_NEAR_CANDIDATE_PREWARM=true # Prewarm block hex for near-candidate shares
NEAR_CANDIDATE_PREWARM_FACTOR=256 # Prewarm if share is within target * factor
GBT_RULES=segwit # getblocktemplate rules (comma-separated)LOG_LEVEL=info # Logging level: debug|info|warn|error
LOG_FORMAT=auto # auto|json|pretty (auto=pretty on TTY, json otherwise)
DEBUG_SHARE_VALIDATION=false # Extra diagnostics for share validation issuesSTATS_PERSISTENCE_ENABLED=true # Persist pool stats to disk using snapshot + WAL
STATS_PERSISTENCE_DIR=data # Base directory for persistence files
STATS_WAL_CAPTURE_MS=1000 # Capture interval for WAL records (in-memory snapshot)
STATS_WAL_FLUSH_MS=1000 # WAL flush interval to disk
STATS_CHECKPOINT_MS=60000 # Full snapshot interval; WAL is truncated after checkpoint
STATS_RECENT_SHARES_MAX=240 # Max recent share samples retained in snapshots- Add native hash validators for other DigiByte algos (scrypt/skein/qubit/odocrypt) via optional addon.
- Pin pool container to isolated CPU cores and use host networking in Coolify (if acceptable).
- Run the pool on the same box as the DigiByte node or same L2 switch path.
- Improve vardiff tuning/controls (smoother retargeting, per-miner persistence, anti-burst heuristics).
-
Check
STRATUM_PREVHASH_MODE- try different modes:stratum(default, most common)stratum_wordrev(some BM1370 firmware)headerorrpc(rare)
-
Enable debug logging:
LOG_LEVEL=debug DEBUG_SHARE_VALIDATION=true -
Check miner difficulty matches pool difficulty
- "max clients reached": Increase
MAX_CLIENTS - "max clients per IP": Increase
MAX_CLIENTS_PER_IPor check for IP conflicts - "rate limit exceeded": Increase
CONNECTION_RATE_LIMIT_PER_MINor investigate connection spam
- Ensure miners are actually hashing (check dashboard for shares)
- This is solo mining - blocks are rare! Expected time:
network_difficulty / your_hashrate / 600seconds - Check
pool_blocks_rejected_totalmetric - if >0, investigate node logs
- The Docker image now sets writable ownership on
/app, so new deployments should not hit this. - For existing deployments, rebuild/redeploy the image so the updated Dockerfile takes effect.
- Optional: set
STATS_PERSISTENCE_DIRto a known writable path and mount it as a persistent volume.
- Check dashboard performance metrics at
/metrics - Dashboard uses Server-Sent Events (SSE) for real-time updates with minimal overhead
/statsendpoint still available for polling-based integrations- Consider adding reverse proxy cache for
/statsif used by external tools
- Verify certificate and key paths are correct
- Check file permissions (pool user must be able to read cert files)
- Ensure
API_TLS_CERTandAPI_TLS_KEYboth point to valid files - Test certificate:
openssl x509 -in cert.pem -text -noout
Contributions welcome! Please:
- Follow existing code style
- Test changes with real miners if modifying Stratum code
- Document new environment variables in
.env.example
This software is provided as-is for self-hosted solo mining. Use at your own risk. Always verify the code before running in production, and never expose your mining pool directly to the internet without proper security measures.