#!/bin/bash # # Bitcoin Node for Solo Miners Appliance Installer # An Open Source Linux Appliance from Robbie Ferguson # (c) 2025 Robbie Ferguson # Version 1.0.3 set -e BTC_VER="29.2" APP_ROOT="/opt/btc-solo" WWW_ROOT="/var/www/btc-solo" BITCOIN_CONF="/etc/bitcoin/bitcoin.conf" CERT_DIR="/etc/ssl/localcerts" CKPOOL_USER="ckpool" CKPOOL_PORT="3333" BITCOIN_DATA="/var/lib/bitcoind" BTC_RPC_USER="user" BTC_RPC_PASS="pass" PRUNE_SIZE="550" # --------------------------------------------------------------------------- # PURGE # --------------------------------------------------------------------------- if [[ "$1" == "--purge" ]]; then systemctl stop ckpool 2>/dev/null || true systemctl stop bitcoind 2>/dev/null || true rm -f /var/lib/btc-solo/.setup-complete rm -f /etc/systemd/system/ckpool.service rm -f /etc/systemd/system/bitcoind.service rm -f /etc/nginx/sites-enabled/btc-solo rm -f /etc/nginx/sites-available/btc-solo rm -f /etc/sudoers.d/btc-solo rm -rf "$APP_ROOT" "$WWW_ROOT" "$CERT_DIR" "$BITCOIN_DATA" rm -f /usr/local/sbin/btc-apply-admin.sh /usr/local/sbin/btc-set-prune.sh /usr/local/sbin/btc-make-cert.sh # Remove bitcoin binaries we installed directly rm -f /usr/local/bin/bitcoind /usr/local/bin/bitcoin-cli /usr/local/bin/bitcoin-tx /usr/local/bin/bitcoin-util 2>/dev/null || true systemctl daemon-reload systemctl restart nginx 2>/dev/null || true echo "Bitcoin Solo Miner Appliance purged." exit 0 fi if [[ $EUID -ne 0 ]]; then echo "Run as root." exit 1 fi echo "[***] Updating apt…" apt-get update echo "[***] Installing base packages…" apt-get install -y \ curl ca-certificates gnupg \ nginx php-fpm php-cli \ git build-essential autoconf automake libtool pkg-config \ libjansson-dev libevent-dev libcurl4-openssl-dev libssl-dev zlib1g-dev \ openssl jq bc sqlite3 php-sqlite3 # --------------------------------------------------------------------------- # Install Bitcoin Core from official binaries # --------------------------------------------------------------------------- echo "[***] Installing official Bitcoin Core binaries…" # ===== Detect CPU arch and install correct Bitcoin Core binary ===== detect_arch() { # Prefer dpkg (Debian/Ubuntu), fall back to uname local deb_arch deb_arch=$(dpkg --print-architecture 2>/dev/null || true) case "$deb_arch" in amd64) echo "x86_64-linux-gnu"; return ;; arm64) echo "aarch64-linux-gnu"; return ;; armhf) echo "arm-linux-gnueabihf"; return ;; esac # Fallback by kernel arch local u u=$(uname -m) case "$u" in x86_64) echo "x86_64-linux-gnu" ;; aarch64) echo "aarch64-linux-gnu" ;; armv7l|armv6l) echo "arm-linux-gnueabihf" ;; *) echo "unsupported" ;; esac } BTC_PLAT="$(detect_arch)" if [ "$BTC_PLAT" = "unsupported" ]; then echo "ERROR: Unsupported CPU architecture ($(uname -m))." echo "Tip: For exotic arches, build from source using depends/ then install bitcoind/bitcoin-cli." exit 1 fi BASE_URL="https://bitcoincore.org/bin/bitcoin-core-$BTC_VER" TARBALL="bitcoin-$BTC_VER-$BTC_PLAT.tar.gz" TMPD="$(mktemp -d)" echo "[Bitcoin Core] Detected platform: $BTC_PLAT" echo "[Bitcoin Core] Downloading $TARBALL …" curl -fsSL "$BASE_URL/$TARBALL" -o "$TMPD/$TARBALL" # Get SHA256SUMS (and try to verify signature if gpg is present) curl -fsSL "$BASE_URL/SHA256SUMS" -o "$TMPD/SHA256SUMS" if command -v gpg >/dev/null 2>&1; then curl -fsSL "$BASE_URL/SHA256SUMS.asc" -o "$TMPD/SHA256SUMS.asc" || true # If you've already imported the Bitcoin Core release keys, this will verify. # If not, we still proceed after SHA256 check below. gpg --verify "$TMPD/SHA256SUMS.asc" "$TMPD/SHA256SUMS" || echo "[WARN] Could not verify SHA256SUMS signature (no keys?). Continuing with SHA256 check." fi echo "[Bitcoin Core] Verifying tarball checksum …" ( cd "$TMPD" && grep " $TARBALL" SHA256SUMS | sha256sum -c - ) echo "[Bitcoin Core] Installing to /usr/local/bin …" tar -xzf "$TMPD/$TARBALL" -C "$TMPD" install -m 0755 "$TMPD/bitcoin-$BTC_VER/bin/bitcoind" /usr/local/bin/ install -m 0755 "$TMPD/bitcoin-$BTC_VER/bin/bitcoin-cli" /usr/local/bin/ install -m 0755 "$TMPD/bitcoin-$BTC_VER/bin/bitcoin-tx" /usr/local/bin/ install -m 0755 "$TMPD/bitcoin-$BTC_VER/bin/bitcoin-wallet" /usr/local/bin/ rm -rf "$TMPD" echo "[Bitcoin Core] Installed bitcoind/bitcoin-cli for $BTC_PLAT" # systemd service for bitcoind cat > /etc/systemd/system/bitcoind.service < "$BITCOIN_CONF" </dev/null 2>&1 || useradd -r -s /bin/false "$CKPOOL_USER" usermod -aG $CKPOOL_USER www-data # try to restart the running PHP-FPM service so www-data picks up new group php_fpm_service="" # 1) ask systemd what php-fpm services exist php_fpm_service=$(systemctl list-units --type=service --all \ | awk '/php.*fpm\.service/ {print $1; exit}') if [ -n "$php_fpm_service" ]; then systemctl restart "$php_fpm_service" fi mkdir -p "$APP_ROOT/conf" "$APP_ROOT/logs" cat > "$APP_ROOT/conf/ckpool.conf" < /usr/local/sbin/btc-save-history.sh <<'EOF' #!/bin/bash APP_ROOT="/opt/btc-solo" HISTDB="$APP_ROOT/db/hashrate_history.sqlite" USERS_DIR="$APP_ROOT/logs/users" CKPOOL_PHP="/var/www/btc-solo/ckpool_status.php" # bail quietly if prerequisites are missing for bin in sqlite3 jq php; do command -v "$bin" >/dev/null 2>&1 || exit 0 done [ -f "$HISTDB" ] || exit 0 [ -d "$USERS_DIR" ] || exit 0 [ -f "$CKPOOL_PHP" ] || exit 0 TS=$(date +%s) insert_row() { local ts="$1" addr="$2" json="$3" b64 if base64 --help 2>&1 | grep -q '\-w'; then b64=$(printf '%s' "$json" | base64 -w0) else b64=$(printf '%s' "$json" | base64) fi sqlite3 "$HISTDB" \ "INSERT INTO history (ts,address,payload) VALUES ($ts,'$addr','$b64');" \ 2>/dev/null || true } # 1) Pool snapshot (no addr = global pool) POOL_JSON=$(php "$CKPOOL_PHP" 2>/dev/null) if [ -n "$POOL_JSON" ]; then ERR=$(printf '%s' "$POOL_JSON" | jq -r '.error // empty' 2>/dev/null) if [ -z "$ERR" ]; then insert_row "$TS" "__POOL__" "$POOL_JSON" fi fi # 2) Per-address snapshots for worker history for f in "$USERS_DIR"/*; do [ -f "$f" ] || continue ADDR=$(basename "$f") [ -n "$ADDR" ] || continue JSON=$(ADDR="$ADDR" php -r '$_GET["addr"] = getenv("ADDR"); include "/var/www/btc-solo/ckpool_status.php";' 2>/dev/null) [ -n "$JSON" ] || continue ERR=$(printf '%s' "$JSON" | jq -r '.error // empty' 2>/dev/null) [ -n "$ERR" ] && continue insert_row "$TS" "$ADDR" "$JSON" done exit 0 EOF chmod 0755 /usr/local/sbin/btc-save-history.sh echo "[***] Setting up Cron Jobs..." # Cron job for hashrate history logging cat > /etc/cron.d/btc-save-history <<'EOF' * * * * * www-data /usr/local/sbin/btc-save-history.sh >/dev/null 2>&1 EOF chmod 0644 /etc/cron.d/btc-save-history echo "Configuring Log Rotation" cat > /etc/logrotate.d/btc-solo <<'EOF' /opt/btc-solo/logs/*.log { weekly rotate 8 compress missingok notifempty copytruncate } EOF cat > /etc/logrotate.d/nginx <<'EOF' /var/log/nginx/*.log { weekly rotate 8 compress missingok notifempty copytruncate } EOF # make ckpmsg available to PHP ln -sf ${APP_ROOT}/${CKPOOL_USER}/src/ckpmsg /usr/local/bin/ckpmsg cat > /etc/systemd/system/ckpool.service < /usr/local/sbin/btc-apply-admin.sh <<'EOF' #!/bin/bash CONF="/etc/bitcoin/bitcoin.conf" CKPOOL_CONF="/opt/btc-solo/conf/ckpool.conf" ADMINUSER="$1" ADMINPASS="$2" if [[ -z "$ADMINUSER" || -z "$ADMINPASS" ]]; then echo "Usage: $0 " exit 1 fi # Update bitcoin.conf sed -i '/^rpcuser=/d' "$CONF" sed -i '/^rpcpassword=/d' "$CONF" echo "rpcuser=${ADMINUSER}" >> "$CONF" echo "rpcpassword=${ADMINPASS}" >> "$CONF" # Update ckpool config so it can still talk to bitcoind if [[ -f "$CKPOOL_CONF" ]]; then sed -i "s/\"user\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"user\" : \"${ADMINUSER}\"/" "$CKPOOL_CONF" sed -i "s/\"pass\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"pass\" : \"${ADMINPASS}\"/" "$CKPOOL_CONF" fi systemctl restart bitcoind systemctl restart ckpool EOF chmod 700 /usr/local/sbin/btc-apply-admin.sh # set prune size cat > /usr/local/sbin/btc-set-prune.sh <<'EOF' #!/bin/bash CONF="/etc/bitcoin/bitcoin.conf" SIZE="$1" if [[ -z "$SIZE" ]]; then echo "Usage: $0 " exit 1 fi sed -i '/^prune=/d' "$CONF" if [[ "$SIZE" != "full" ]]; then echo "prune=${SIZE}" >> "$CONF" fi systemctl restart bitcoind EOF chmod 700 /usr/local/sbin/btc-set-prune.sh # setup donation system cat > /usr/local/sbin/btc-set-donation.sh <<'EOF' #!/bin/bash CONF="/opt/btc-solo/conf/ckpool.conf" ADDR="$1" RATE="$2" if [[ -z "$ADDR" || -z "$RATE" ]]; then echo "Usage: $0 " exit 1 fi # ensure conf exists if [[ ! -f "$CONF" ]]; then echo "ckpool.conf not found at $CONF" exit 1 fi # remove old lines sed -i '/"donaddress"/d' "$CONF" sed -i '/"donrate"/d' "$CONF" # add new ones just before the final } # (very simple appender; good enough for our generated file) sed -i '$i \ ,"donaddress" : "'"$ADDR"'",\n "donrate" : '"$RATE"'' "$CONF" systemctl restart ckpool EOF chmod 700 /usr/local/sbin/btc-set-donation.sh # (re)create self-signed cert cat > /usr/local/sbin/btc-make-cert.sh <<'EOF' #!/bin/bash CERTDIR="/etc/ssl/localcerts" mkdir -p "$CERTDIR" openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout ${CERTDIR}/btc-solo.key \ -out ${CERTDIR}/btc-solo.crt \ -subj "/C=CA/ST=Ontario/L=Barrie/O=BTC-Solo/OU=IT/CN=$(hostname -f)" chmod 600 ${CERTDIR}/btc-solo.key systemctl restart nginx EOF chmod 700 /usr/local/sbin/btc-make-cert.sh # ensure sudoers.d exists mkdir -p /etc/sudoers.d chmod 750 /etc/sudoers.d # sudoers cat > /etc/sudoers.d/btc-solo <<'EOF' www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-apply-admin.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-set-prune.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-make-cert.sh www-data ALL=(root) NOPASSWD: /usr/local/sbin/btc-set-donation.sh EOF chmod 440 /etc/sudoers.d/btc-solo # --------------------------------------------------------------------------- echo "[***] Web UI (dashboard + activate)" # --------------------------------------------------------------------------- # web-owned state dir mkdir -p /var/lib/btc-solo chown www-data:www-data /var/lib/btc-solo # web dashboard mkdir -p "$WWW_ROOT" mkdir -p "$WWW_ROOT/activate" # Main dashboard cat > "$WWW_ROOT/index.php" <<'EOF' Bitcoin Node for Solo Miners

Bitcoin Node for Solo Miners

A Linux Appliance by Robbie Ferguson

Node Status blockchain & system

Loading status…

Connect Your Miners stratum endpoint

Endpoint

Username

Your BTC payout address

Password

anything

Your miners will start submitting shares once Bitcoin Core is fully synced and connected.

Workers & Pool mining overview

Pulling pool stats from CKPool logs...

Best of luck to you!

EOF # Settings page cat > "$WWW_ROOT/settings.php" <<'EOF' '550 MB (default, small)', (2*1024) => '2 GB', (4*1024) => '4 GB', (8*1024) => '8 GB', (16*1024) => '16 GB', (32*1024) => '32 GB', (64*1024) => '64 GB', (100*1024) => '100 GB', (200*1024) => '200 GB', (300*1024) => '300 GB', (400*1024) => '400 GB', 'full' => 'Full (no pruning)', ]; $current = '550'; $conf = @file_get_contents('/etc/bitcoin/bitcoin.conf'); if ($conf && preg_match('/^prune=(\d+)/m', $conf, $m)) { $current = $m[1]; } elseif ($conf && !preg_match('/^prune=/m', $conf)) { $current = 'full'; } if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['prune'])) { $sel = $_POST['prune']; if (isset($opts[$sel])) { shell_exec('sudo /usr/local/sbin/btc-set-prune.sh ' . escapeshellarg($sel)); $current = $sel; sleep(1); } } $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? gethostname(); $host = preg_replace('/:\d+$/', '', $host); // strip port if present $stratum = 'stratum+tcp://' . $host . ':3333'; ?> Bitcoin Node for Solo Miners [Settings]

Bitcoin Node for Solo Miners

A Linux Appliance by Robbie Ferguson

Stratum Endpoint

Username: BTC address for payout

Password: anything

Blockchain Prune Size

Note: Prune size only limits stored block data. You should also expect the node to use up to 500 MB for chainstate and metadata. Actual disk usage will be higher than the value selected here.

Changing this will restart bitcoind.

Donate to Bald Nerd

If you happen to win a BTC block, you can donate a small percentage to Robbie Ferguson (developer of this appliance, host of Category5 Technology TV) with big thanks. You can set to 0 if you prefer to keep the full block to yourself.

EOF # AJAX status info cat > "$WWW_ROOT/status.php" <<'EOF' 'not activated']); exit; } header('Content-Type: application/json'); // ---- 1) bitcoin core info ---- $btcInfo = null; $out = shell_exec('/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf getblockchaininfo 2>/dev/null'); if ($out) { $btcInfo = json_decode($out, true); } // ---- 2) service health ---- function svc($name) { $res = trim(shell_exec('systemctl is-active ' . escapeshellarg($name) . ' 2>/dev/null')); return $res === 'active' ? 'active' : $res; } $services = [ 'bitcoind' => svc('bitcoind'), 'ckpool' => svc('ckpool'), ]; // ---- 3) system info (non-sensitive) ---- // uptime $uptime_seconds = null; if (is_readable('/proc/uptime')) { $up = trim(file_get_contents('/proc/uptime')); $parts = explode(' ', $up); $uptime_seconds = (int)floatval($parts[0]); } // load average $load = function_exists('sys_getloadavg') ? sys_getloadavg() : null; // memory $mem_total = null; $mem_avail = null; if (is_readable('/proc/meminfo')) { $meminfo = file('/proc/meminfo', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); foreach ($meminfo as $line) { if (strpos($line, 'MemTotal:') === 0) { $mem_total = (int)filter_var($line, FILTER_SANITIZE_NUMBER_INT) * 1024; } elseif (strpos($line, 'MemAvailable:') === 0) { $mem_avail = (int)filter_var($line, FILTER_SANITIZE_NUMBER_INT) * 1024; } } } // disk (root) $disk_root_total = @disk_total_space('/'); $disk_root_free = @disk_free_space('/'); // disk (bitcoind datadir) $btcDir = '/var/lib/bitcoind'; $disk_btc_total = @disk_total_space($btcDir); $disk_btc_free = @disk_free_space($btcDir); // bitcoind dir size (used) $btc_dir_size = null; $du = shell_exec('du -sb ' . escapeshellarg($btcDir) . ' 2>/dev/null'); if ($du) { $parts = preg_split('/\s+/', trim($du)); if (isset($parts[0]) && ctype_digit($parts[0])) { $btc_dir_size = (int)$parts[0]; } } // CPU temperature (if available, eg. Raspberry Pi) $cpu_temp_c = null; foreach (glob('/sys/class/thermal/thermal_zone*/temp') as $zone) { $raw = @file_get_contents($zone); if ($raw === false) continue; $raw = trim($raw); if ($raw === '' || !is_numeric($raw)) continue; $val = (float)$raw; // Many systems report millidegC if ($val > 200) $val = $val / 1000.0; if ($val > 0 && $val < 120) { $cpu_temp_c = $val; break; } } // ---- 4) output ---- echo json_encode([ 'initialblockdownload' => $btcInfo['initialblockdownload'] ?? null, 'blocks' => $btcInfo['blocks'] ?? null, 'headers' => $btcInfo['headers'] ?? null, 'verificationprogress' => $btcInfo['verificationprogress'] ?? null, 'pruned' => $btcInfo['pruned'] ?? null, 'services' => $services, 'system' => [ 'uptime_seconds' => $uptime_seconds, 'loadavg' => $load, 'mem_total_bytes' => $mem_total, 'mem_available_bytes' => $mem_avail, 'disk_root' => [ 'total_bytes' => $disk_root_total, 'free_bytes' => $disk_root_free, ], 'disk_bitcoind' => [ 'total_bytes' => $disk_btc_total, 'free_bytes' => $disk_btc_free, // Prefer filesystem view (matches `df`); fall back to du if needed 'used_bytes' => ($disk_btc_total !== false && $disk_btc_free !== false) ? ($disk_btc_total - $disk_btc_free) : $btc_dir_size, ], 'cpu_temp_c' => $cpu_temp_c, ], ], JSON_UNESCAPED_SLASHES); EOF # CKPool Status # (reads CKPool JSON logs; address supplied per-request) --- cat > /var/www/btc-solo/ckpool_status.php <<'PHP' 'logs', 'pool' => null, 'user_addr' => $addr ?: null, 'user' => null, 'workers' => [], 'worker_count' => 0, 'error' => null, ]; // Pool summary (3 JSON lines) $pool_objs = read_json_lines($POOL_STATUS); if ($pool_objs) { $resp['pool'] = [ 'counts' => $pool_objs[0] ?? null, // runtime/users/workers/idle/disconnected 'hashrate' => $pool_objs[1] ?? null, // hashrate1m/5m/15m/1hr/... 'shares' => $pool_objs[2] ?? null, // accepted/rejected/bestshare/SPS... ]; } else { $resp['error'] = 'Cannot read pool.status'; } // User + workers (only if client supplied an address) if ($addr !== '') { $user_file = $USERS_DIR . '/' . $addr; $user_obj = read_json_file($user_file); if ($user_obj) { $resp['user'] = $user_obj; if (!empty($user_obj['worker']) && is_array($user_obj['worker'])) { foreach ($user_obj['worker'] as $w) { $resp['workers'][] = [ 'workername' => $w['workername'] ?? 'unknown', 'hashrate1m' => $w['hashrate1m'] ?? null, 'hashrate5m' => $w['hashrate5m'] ?? null, 'hashrate1hr' => $w['hashrate1hr'] ?? null, 'hashrate1d' => $w['hashrate1d'] ?? null, 'hashrate7d' => $w['hashrate7d'] ?? null, 'lastshare' => $w['lastshare'] ?? null, 'shares' => $w['shares'] ?? null, 'bestshare' => $w['bestshare'] ?? null, 'bestever' => $w['bestever'] ?? null, ]; } } $resp['worker_count'] = count($resp['workers']); } else { // Not a fatal error - just "no data yet" for that address if ($resp['error'] === null) { $resp['error'] = 'No worker stats yet for this address'; } } } echo json_encode($resp); PHP chmod 0644 /var/www/btc-solo/ckpool_status.php # activation page cat > "$WWW_ROOT/activate/index.php" <<'EOF' Create New Admin User

Create New Admin User

This sets the Bitcoin node's administrative (RPC) credentials used to control the server itself.

Keep these private — they grant full access to your node.

These credentials are not used by miners to connect; miners only need your Bitcoin address and can use any password.

EOF # AJAX History Endpoint cat > "$WWW_ROOT/history.php" <<'PHP' 'not activated']); exit; } header('Content-Type: application/json'); $dbPath = '/opt/btc-solo/db/hashrate_history.sqlite'; if (!file_exists($dbPath)) { echo json_encode(['error' => 'no history database']); exit; } try { $db = new SQLite3($dbPath, SQLITE3_OPEN_READONLY); } catch (Exception $e) { echo json_encode(['error' => 'cannot open history database']); exit; } $mode = $_GET['mode'] ?? 'pool'; $limit = (int)($_GET['limit'] ?? 120); if ($limit < 10) $limit = 10; if ($limit > 720) $limit = 720; // up to 12 hours at 1/min function parse_rate(?string $s): ?float { if ($s === null || $s === '') return null; if (!preg_match('/^([0-9.]+)\s*([kMGTPE]?)/i', $s, $m)) return null; $v = (float)$m[1]; $unit = strtoupper($m[2] ?? ''); $mult = [ '' => 1, 'K' => 1e3, 'M' => 1e6, 'G' => 1e9, 'T' => 1e12, 'P' => 1e15, 'E' => 1e18, ][$unit] ?? 1; return $v * $mult; } function downsample(array $points, int $limit): array { $n = count($points); if ($n <= $limit) return $points; $step = (int)ceil($n / $limit); $out = []; for ($i = 0; $i < $n; $i += $step) { $out[] = $points[$i]; } return $out; } if ($mode === 'worker') { $addr = $_GET['addr'] ?? ''; $worker = $_GET['worker'] ?? ''; if ($addr === '' || $worker === '') { echo json_encode(['error' => 'missing addr or worker']); exit; } $stmt = $db->prepare( 'SELECT ts,payload FROM history WHERE address = :addr ORDER BY ts DESC LIMIT :lim' ); $rawLimit = $limit * 4; $stmt->bindValue(':addr', $addr, SQLITE3_TEXT); $stmt->bindValue(':lim', $rawLimit, SQLITE3_INTEGER); $res = $stmt->execute(); $points = []; while ($row = $res->fetchArray(SQLITE3_ASSOC)) { $ts = (int)$row['ts']; $payload = base64_decode($row['payload'], true); if ($payload === false) continue; $obj = json_decode($payload, true); if (!is_array($obj) || empty($obj['workers']) || !is_array($obj['workers'])) continue; $match = null; foreach ($obj['workers'] as $w) { if (!isset($w['workername'])) continue; if ($w['workername'] === $worker) { $match = $w; break; } } if (!$match) continue; $points[] = [ 'ts' => $ts, 'h1m' => parse_rate($match['hashrate1m'] ?? null), 'h5m' => parse_rate($match['hashrate5m'] ?? null), 'h1h' => parse_rate($match['hashrate1hr'] ?? null), 'h1d' => parse_rate($match['hashrate1d'] ?? null), 'h7d' => parse_rate($match['hashrate7d'] ?? null), ]; } usort($points, fn($a,$b) => $a['ts'] <=> $b['ts']); $points = downsample($points, $limit); echo json_encode(['mode' => 'worker', 'addr' => $addr, 'worker' => $worker, 'points' => $points]); exit; } // Default: pool history $stmt = $db->prepare( 'SELECT ts,payload FROM history WHERE address = :addr ORDER BY ts DESC LIMIT :lim' ); $rawLimit = $limit * 4; $stmt->bindValue(':addr', '__POOL__', SQLITE3_TEXT); $stmt->bindValue(':lim', $rawLimit, SQLITE3_INTEGER); $res = $stmt->execute(); $seen = []; $points = []; while ($row = $res->fetchArray(SQLITE3_ASSOC)) { $ts = (int)$row['ts']; if (isset($seen[$ts])) continue; $seen[$ts] = true; $payload = base64_decode($row['payload'], true); if ($payload === false) continue; $obj = json_decode($payload, true); if (!is_array($obj) || empty($obj['pool']['hashrate'])) continue; $h = $obj['pool']['hashrate']; $points[] = [ 'ts' => $ts, 'h1m' => parse_rate($h['hashrate1m'] ?? null), 'h5m' => parse_rate($h['hashrate5m'] ?? null), 'h15m'=> parse_rate($h['hashrate15m'] ?? null), 'h1h' => parse_rate($h['hashrate1hr'] ?? null), 'h1d' => parse_rate($h['hashrate1d'] ?? null), 'h7d' => parse_rate($h['hashrate7d'] ?? null), ]; } usort($points, fn($a,$b) => $a['ts'] <=> $b['ts']); $points = downsample($points, $limit); echo json_encode(['mode' => 'pool', 'points' => $points]); PHP # --------------------------------------------------------------------------- echo "Console Dashboard" # --------------------------------------------------------------------------- cat > /usr/local/sbin/btc-console-dashboard <<'EOF' #!/bin/bash # Console dashboard for Bitcoin Solo Miner BITCOIN_CLI="/usr/local/bin/bitcoin-cli" BITCOIN_CONF="/etc/bitcoin/bitcoin.conf" BTC_DIR="/var/lib/bitcoind" # colors RED="\033[31m" GRN="\033[32m" YEL="\033[33m" BLU="\033[34m" CYN="\033[36m" BOLD="\033[1m" RST="\033[0m" CONSEC_FAILS=0 MAX_FAILS=3 LAST_INFO="" get_ip() { ip -4 addr show scope global 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1 | head -n1 } make_bar() { local pct="$1" local width="$2" [ -z "$pct" ] && pct=0 pct=$(printf "%.1f" "$pct") # clamp if (( $(echo "$pct < 0" | bc -l 2>/dev/null || echo 0) )); then pct=0; fi if (( $(echo "$pct > 100" | bc -l 2>/dev/null || echo 0) )); then pct=100; fi local filled=$(printf "%.0f" "$(echo "$pct / 100 * $width" | bc -l)") local empty=$((width - filled)) printf "[" for ((i=0; i/dev/null || true tput cup 0 0; echo -e "${BOLD}Bitcoin Node for Solo Miners${RST}" tput cup 1 0; echo -e "A Linux Appliance by Robbie Ferguson" tput cup 2 0; printf '%*s' "$(tput cols)" '' | tr ' ' '-' } draw_loading() { tput cup 3 0; echo -e "${YEL}Loading node status…${RST}\033[K" } draw_dashboard() { # args: # 1 now 2 ip 3 web 4 status 5 bar 6 progress 7 services 8 cpu 9 mem 10 disk 11 workers local now="$1" local ip="$2" local web="$3" local status="$4" local bar="$5" local progress="$6" local services="$7" local cpu="$8" local mem="$9" local disk="${10}" local workers="${11}" tput cup 3 0; echo -e "Time: ${CYN}${now}${RST}\033[K" tput cup 4 0; echo -e "IP: ${CYN}${ip}${RST}\033[K" tput cup 5 0; echo -e "Web: ${CYN}${web}${RST}\033[K" tput cup 6 0; printf '%*s' "$(tput cols)" '' | tr ' ' '-' tput cup 7 0; echo -e "${status}\033[K" tput cup 8 0; echo -e "${bar}\033[K" tput cup 9 0; echo -e "${progress}\033[K" tput cup 10 0; echo -e "${services}\033[K" tput cup 11 0; printf '%*s' "$(tput cols)" '' | tr ' ' '-' tput cup 12 0; echo -e "CPU load: ${cpu}\033[K" tput cup 13 0; echo -e "Memory: ${mem}\033[K" tput cup 14 0; echo -e "Disk (${BTC_DIR}): ${disk}\033[K" tput cup 16 0; echo -e "Mining:\033[K" tput cup 17 2; echo -e "${workers}\033[K" # tput cup 19 0; echo -e "(Appliance console view — use the web UI for full details.)\033[K" } draw_header draw_loading FIRST_DONE=0 while true; do # -------- collect data (non-blocking) -------- NOW=$(date '+%Y-%m-%d %H:%M') IPADDR=$(get_ip) [ -z "$IPADDR" ] && IPADDR="No network" WEB="https://${IPADDR}/" BTC_STATE=$(systemctl is-active bitcoind 2>/dev/null || true) CKP_STATE=$(systemctl is-active ckpool 2>/dev/null || true) STATUS_LINE="" PROGRESS_LINE="" BAR_LINE="[--------------] 0.0%" SERVICES_LINE="Bitcoin: ${BTC_STATE} CKPool: ${CKP_STATE}" # --- CKPool worker summary (console-safe, no per-worker listing) --- POOL_STATUS="/opt/btc-solo/logs/pool/pool.status" # Humanize hashrate (expects H/s as a number; falls back to raw if unknown) human_hashrate() { local v="$1" if [[ -z "$v" || "$v" == "null" ]]; then echo "-"; return; fi if ! [[ "$v" =~ ^[0-9]+([.][0-9]+)?$ ]]; then echo "$v"; return; fi awk -v x="$v" 'BEGIN{ u[0]="H/s"; u[1]="kH/s"; u[2]="MH/s"; u[3]="GH/s"; u[4]="TH/s"; u[5]="PH/s"; u[6]="EH/s"; i=0; while (x>=1000 && i<6) { x/=1000; i++ } if (x<10) printf("%.2f %s", x, u[i]); else if (x<100)printf("%.1f %s", x, u[i]); else printf("%.0f %s", x, u[i]); }' } # Add commas to large integers (e.g. 1000000 -> 1,000,000) fmt_int() { [[ "$1" =~ ^[0-9]+$ ]] && printf "%'d" "$1" || printf "%s" "$1" } if command -v jq >/dev/null 2>&1; then WORKERS_LINE="No workers detected (point your ASICs at this node)" else WORKERS_LINE="Install jq for worker stats." fi if [[ -r "$POOL_STATUS" ]]; then # pool.status is 3 JSON objects, one per line: counts, hashrate, shares # Slurp into array so we can index [0],[1],[2] POOL_JSON="$(jq -s '.' "$POOL_STATUS" 2>/dev/null || true)" if [[ -n "$POOL_JSON" && "$POOL_JSON" != "null" ]]; then workers="$(jq -r '.[0].Workers // empty' <<<"$POOL_JSON")" idle="$(jq -r '.[0].idle // empty' <<<"$POOL_JSON")" disc="$(jq -r '.[0].disconnected // empty' <<<"$POOL_JSON")" users="$(jq -r '.[0].users // empty' <<<"$POOL_JSON")" hr1m="$(jq -r '.[1].hashrate1m // empty' <<<"$POOL_JSON")" hr5m="$(jq -r '.[1].hashrate5m // empty' <<<"$POOL_JSON")" hr15m="$(jq -r '.[1].hashrate15m // empty' <<<"$POOL_JSON")" acc="$(jq -r '.[2].accepted // empty' <<<"$POOL_JSON")" rej="$(jq -r '.[2].rejected // empty' <<<"$POOL_JSON")" stale="$(jq -r '.[2].stale // empty' <<<"$POOL_JSON")" dup="$(jq -r '.[2].dup // empty' <<<"$POOL_JSON")" sps="$(jq -r '.[2].SPS1m // empty' <<<"$POOL_JSON")" bestshare="$(jq -r '.[2].bestshare // empty' <<<"$POOL_JSON")" if [[ -n "$workers" && "$workers" != "0" ]]; then # Compact worker line (single line, console-safe) # Example: Workers: 3 (idle 1, disc 0) | HR: 1m 1.2 TH/s / 5m 1.1 TH/s / 15m 1.0 TH/s | Shares: acc 120 rej 0 stale 1 dup 0 | SPS 0.03 WORKERS_LINE="Workers: ${workers}" [[ -n "$idle" || -n "$disc" ]] && WORKERS_LINE+=" (idle ${idle:-0}, disc ${disc:-0})" WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter [[ -n "$users" ]] && WORKERS_LINE+="\n Users: ${users}" WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter WORKERS_LINE+="\n Hashrate: 1m $(human_hashrate "$hr1m") / 5m $(human_hashrate "$hr5m") / 15m $(human_hashrate "$hr15m")" WORKERS_LINE+=" " # Extend a little beyond the end of the current text to blank if new output is shorter # Shares (show only fields that exist; keep it tight) WORKERS_LINE+="\n Shares:" WORKERS_LINE+="\n Accepted: $(fmt_int "${acc:-0}") " [[ -n "$rej" ]] && WORKERS_LINE+="\n Rejected: $(fmt_int "$rej") " [[ -n "$stale" ]] && WORKERS_LINE+="\n Stale: $(fmt_int "$stale") " [[ -n "$dup" ]] && WORKERS_LINE+="\n Duplicate:$(fmt_int "$dup") " [[ -n "$sps" ]] && WORKERS_LINE+="\n Shares Per Second: $sps " [[ -n "$bestshare" ]] && WORKERS_LINE+="\n Best Share: $(fmt_int "$bestshare") " fi fi fi # --- /CKPool worker summary --- # cpu load if [ -r /proc/loadavg ]; then read -r L1 L5 L15 _ < /proc/loadavg CPU_LINE="${L1} ${L5} ${L15}" else CPU_LINE="n/a" fi # memory if [ -r /proc/meminfo ]; then MEM_TOTAL=$(awk '/MemTotal:/{print $2}' /proc/meminfo) MEM_AVAIL=$(awk '/MemAvailable:/{print $2}' /proc/meminfo) if [ -n "$MEM_TOTAL" ] && [ -n "$MEM_AVAIL" ]; then MEM_USED=$((MEM_TOTAL - MEM_AVAIL)) MEM_PCT=$((MEM_USED * 100 / MEM_TOTAL)) MEM_LINE="$(printf "%dMiB / %dMiB (%d%% used)" "$((MEM_USED/1024))" "$((MEM_TOTAL/1024))" "$MEM_PCT")" else MEM_LINE="n/a" fi else MEM_LINE="n/a" fi # disk if df -h "$BTC_DIR" >/dev/null 2>&1; then read -r _ SIZE USED AVAIL USEP MNT <<< "$(df -h "$BTC_DIR" | awk 'NR==2{print $1,$2,$3,$4,$5,$6}')" DISK_LINE="${AVAIL} free of ${SIZE} (${USEP} used)" else DISK_LINE="n/a" fi # bitcoin-cli, but don't let it hang INFO=$(timeout 15 "$BITCOIN_CLI" -conf="$BITCOIN_CONF" getblockchaininfo 2>/dev/null) if [ -z "$INFO" ]; then # failed this round CONSEC_FAILS=$((CONSEC_FAILS + 1)) # if we have an older good value, reuse it if [ -n "$LAST_INFO" ]; then INFO="$LAST_INFO" fi else # success - store and reset fail counter LAST_INFO="$INFO" CONSEC_FAILS=0 fi if [ "$BTC_STATE" != "active" ]; then STATUS_LINE="${RED}❌ Bitcoin service is not running.${RST}" elif [ -z "$INFO" ]; then if [ $CONSEC_FAILS -ge $MAX_FAILS ]; then STATUS_LINE="${RED}❌ bitcoin-cli has not responded for several checks.${RST}" BAR_LINE="[--------------] 0.0%" PROGRESS_LINE="" else # transient hiccup — keep showing last good data, or at least don't scream STATUS_LINE="${YEL}⚠ Waiting for Bitcoin Core…${RST}" fi else if command -v jq >/dev/null 2>&1; then IBD=$(echo "$INFO" | jq -r '.initialblockdownload') BLOCKS=$(echo "$INFO" | jq -r '.blocks') HEADERS=$(echo "$INFO" | jq -r '.headers') PROG=$(echo "$INFO" | jq -r '.verificationprogress') PRUNED=$(echo "$INFO" | jq -r '.pruned') if [ "$PROG" != "null" ]; then PROG_PCT=$(printf "%.1f" "$(echo "$PROG * 100" | bc -l 2>/dev/null || echo 0)") else PROG_PCT="0.0" fi if [ "$IBD" = "true" ]; then if [ "$(echo "$PROG_PCT < 0.1" | bc -l 2>/dev/null || echo 0)" = "1" ]; then STATUS_LINE="${YEL}⚠ Bitcoin Core is starting up and loading blockchain data...${RST}" else STATUS_LINE="${YEL}⚠ Syncing blockchain... ${PROG_PCT}%${RST}" fi else STATUS_LINE="${GRN}✔ Bitcoin Core is in sync.${RST}" fi PROGRESS_LINE="Blocks: ${BLOCKS} / ${HEADERS} $( [ "$PRUNED" = "true" ] && echo '(pruned)' )" BAR_LINE=$(make_bar "$PROG_PCT" 30) else STATUS_LINE="${CYN}ℹ Bitcoin Core is active (install jq for more detail).${RST}" fi fi # -------- draw -------- draw_dashboard "$NOW" "$IPADDR" "$WEB" "$STATUS_LINE" "$BAR_LINE" "$PROGRESS_LINE" "$SERVICES_LINE" "$CPU_LINE" "$MEM_LINE" "$DISK_LINE" "$WORKERS_LINE" FIRST_DONE=1 sleep 5 done EOF chmod +x /usr/local/sbin/btc-console-dashboard cat > /etc/systemd/system/btc-console-dashboard.service <<'EOF' [Unit] Description=Bitcoin Solo Miner Console Dashboard (dialog) After=network-online.target WantedBy=multi-user.target [Service] Type=simple ExecStart=/usr/local/sbin/btc-console-dashboard StandardInput=tty StandardOutput=tty TTYPath=/dev/tty1 TTYReset=yes TTYVHangup=yes Restart=always [Install] WantedBy=getty.target EOF systemctl daemon-reload systemctl disable getty@tty1.service --now 2>/dev/null || true systemctl enable --now btc-console-dashboard.service # --------------------------------------------------------------------------- echo "NGINX with HTTPS + redirect" # --------------------------------------------------------------------------- mkdir -p "$CERT_DIR" openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ -keyout ${CERT_DIR}/btc-solo.key \ -out ${CERT_DIR}/btc-solo.crt \ -subj "/C=CA/ST=Ontario/L=Barrie/O=BTC-Solo/OU=IT/CN=$(hostname -f)" >/dev/null 2>&1 chmod 600 ${CERT_DIR}/btc-solo.key cat > /etc/nginx/sites-available/btc-solo <