From 81fc4c8313a8bd54de7e55fdb94bc7def1656a94 Mon Sep 17 00:00:00 2001 From: Baldnerd Date: Mon, 29 Dec 2025 12:49:40 -0500 Subject: [PATCH] WIP --- installer | 2066 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 1936 insertions(+), 130 deletions(-) diff --git a/installer b/installer index af99193..84c0326 100755 --- a/installer +++ b/installer @@ -4,6 +4,8 @@ # An Open Source Linux Appliance from Robbie Ferguson # (c) 2025 Robbie Ferguson +# Version 1.0.2 + set -e BTC_VER="30.0" @@ -46,21 +48,21 @@ if [[ $EUID -ne 0 ]]; then exit 1 fi -echo "[1/8] Updating apt…" +echo "[***] Updating apt…" apt-get update -echo "[2/8] Installing base packages…" +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 + openssl jq bc sqlite3 php-sqlite3 # --------------------------------------------------------------------------- -# [3/8] Install Bitcoin Core from official binaries +# Install Bitcoin Core from official binaries # --------------------------------------------------------------------------- -echo "[3/8] Installing official Bitcoin Core binaries…" +echo "[***] Installing official Bitcoin Core binaries…" # ===== Detect CPU arch and install correct Bitcoin Core binary ===== @@ -168,38 +170,39 @@ EOF systemctl daemon-reload systemctl enable bitcoind systemctl restart bitcoind -echo "[3/8] bitcoind started (pruned=550 MB)." +echo "[***] bitcoind started (pruned=550 MB)." # --------------------------------------------------------------------------- -# [4/8] Fetch/build ckpool (solo) - official Bitbucket source +# Fetch/build ckpool (solo) - official Bitbucket source # --------------------------------------------------------------------------- -echo "[4/8] Fetching ckpool from Bitbucket…" +echo "[***] Fetching ckpool from Bitbucket…" mkdir -p "$APP_ROOT" cd "$APP_ROOT" -CKPOOL_URL="https://bitbucket.org/ckolivas/ckpool/get/master.tar.gz" -CKPOOL_TGZ="ckpool.tar.gz" - -# download (Bitbucket requires user agent + redirects) -curl -L -A "btc-solo-installer/1.0" -o "${CKPOOL_TGZ}" "${CKPOOL_URL}" - -# sanity check -if ! file "${CKPOOL_TGZ}" | grep -qi "gzip compressed data"; then - echo "ERROR: ckpool download failed or returned HTML instead of tarball." - head -n 40 "${CKPOOL_TGZ}" || true - exit 1 +if [[ -e ckpool ]]; then + rm -rf ckpool fi -# extract -rm -rf ckpool -mkdir ckpool -tar -xzf "${CKPOOL_TGZ}" --strip-components=1 -C ckpool - +echo "[***] Building ckpool..." +# Obtain the current source code +git clone https://bitbucket.org/ckolivas/ckpool.git ckpool +chown -R $service_user:$service_user ckpool cd ckpool -echo "[4/8] Building ckpool…" + +# This simply sets the donation address to allow you to donate to Robbie Ferguson if you win +# This optional donation (Default 2% of block reward) can be adjusted or turned off in the Settings screen + btc_address="1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP" + find . -type f \( -name '*.c' -o -name '*.h' -o -name '*.cpp' \) -exec \ + sed -i -E \ + "s#(ckp\.donaddress\s*=\s*\")[^\"]*(\";)#\1$btc_address\2#g; + s#(ckp\.tndonaddress\s*=\s*\")[^\"]*(\";)#\1$btc_address\2#g; + s#(ckp\.rtdonaddress\s*=\s*\")[^\"]*(\";)#\1$btc_address\2#g" {} + + +# Build and install CKPool ./autogen.sh -CFLAGS="-O2" ./configure +./configure make +make install # ensure the binary exists and is executable if [[ ! -x "/opt/btc-solo/ckpool/src/ckpool" ]]; then @@ -228,9 +231,12 @@ cat > "$APP_ROOT/conf/ckpool.conf" < @@ -409,50 +427,597 @@ $stratum = 'stratum+tcp://' . $host . ':3333'; Bitcoin Node for Solo Miners

Bitcoin Node for Solo Miners

-

A Linux Appliance by Robbie Ferguson

+

A Linux Appliance by Robbie Ferguson

Settings
-
-

Status

-

Loading status…

-
+
+ +
+

Node Status blockchain & system

+

Loading status…

+
-
-

Workers

-

No workers detected yet. This is normal while Bitcoin Core is syncing or if no ASICs are pointed at this server.

-
+ +
+

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 @@ -827,7 +2278,7 @@ $btcDir = '/var/lib/bitcoind'; $disk_btc_total = @disk_total_space($btcDir); $disk_btc_free = @disk_free_space($btcDir); -// bitcoind dir size +// bitcoind dir size (used) $btc_dir_size = null; $du = shell_exec('du -sb ' . escapeshellarg($btcDir) . ' 2>/dev/null'); if ($du) { @@ -837,6 +2288,22 @@ if ($du) { } } +// 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, @@ -857,12 +2324,112 @@ echo json_encode([ 'disk_bitcoind' => [ 'total_bytes' => $disk_btc_total, 'free_bytes' => $disk_btc_free, - 'used_bytes' => $btc_dir_size, + // 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' 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" # --------------------------------------------------------------------------- @@ -1147,7 +2866,6 @@ while true; do sleep 5 done EOF - chmod +x /usr/local/sbin/btc-console-dashboard cat > /etc/systemd/system/btc-console-dashboard.service <<'EOF' @@ -1220,6 +2938,94 @@ ln -sf /etc/nginx/sites-available/btc-solo /etc/nginx/sites-enabled/btc-solo rm -f /etc/nginx/sites-enabled/default || true systemctl restart nginx +echo "[***] Setting up SQLite Databases" + +# Create SQLite DB for hashrate history +if [ ! -e "$APP_ROOT/db" ]; then + mkdir -p "$APP_ROOT/db" +fi +HISTDB="$APP_ROOT/db/hashrate_history.sqlite" + +if [ ! -f "$HISTDB" ]; then + sqlite3 "$HISTDB" <<'EOSQL' +CREATE TABLE IF NOT EXISTS history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ts INTEGER NOT NULL, + address TEXT NOT NULL, + payload TEXT NOT NULL -- base64-encoded JSON from status.php +); +CREATE INDEX IF NOT EXISTS idx_history_addr_ts ON history(address, ts); +EOSQL + +fi +chown www-data:www-data "$HISTDB" || true + +# Save history script +cat > /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' @@ -1246,7 +3052,7 @@ EOF # --------------------------------------------------------------------------- -echo "[8/8] Done" +echo "[***] Done" # --------------------------------------------------------------------------- echo echo "================================================================"