940 lines
29 KiB
Bash
Executable File
940 lines
29 KiB
Bash
Executable File
#!/bin/bash
|
||
#
|
||
# Bitcoin Node for Solo Miners Appliance Installer
|
||
# An Open Source Linux Appliance from Robbie Ferguson
|
||
# (c) 2025 Robbie Ferguson
|
||
|
||
set -e
|
||
|
||
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 "[1/8] Updating apt…"
|
||
apt-get update
|
||
|
||
echo "[2/8] 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
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# [3/8] Install Bitcoin Core from official binaries
|
||
# ---------------------------------------------------------------------------
|
||
echo "[3/8] Installing official Bitcoin Core binaries…"
|
||
|
||
BTC_VER="27.0"
|
||
BTC_BASE="bitcoin-${BTC_VER}"
|
||
BTC_TAR="${BTC_BASE}-x86_64-linux-gnu.tar.gz"
|
||
BTC_URL="https://bitcoincore.org/bin/bitcoin-core-${BTC_VER}/${BTC_TAR}"
|
||
BTC_SUMS_URL="https://bitcoincore.org/bin/bitcoin-core-${BTC_VER}/SHA256SUMS"
|
||
|
||
mkdir -p /tmp/bitcoin
|
||
cd /tmp/bitcoin
|
||
|
||
echo "Downloading Bitcoin Core ${BTC_VER}…"
|
||
curl -LO "${BTC_URL}"
|
||
curl -LO "${BTC_SUMS_URL}"
|
||
|
||
echo "Verifying checksum…"
|
||
grep "${BTC_TAR}" SHA256SUMS | sha256sum -c -
|
||
|
||
echo "Extracting…"
|
||
tar -xzf "${BTC_TAR}"
|
||
install -m 0755 -o root -g root ${BTC_BASE}/bin/* /usr/local/bin/
|
||
|
||
# systemd service for bitcoind
|
||
cat > /etc/systemd/system/bitcoind.service <<EOF
|
||
[Unit]
|
||
Description=Bitcoin daemon
|
||
After=network.target
|
||
|
||
[Service]
|
||
ExecStart=/usr/local/bin/bitcoind -conf=/etc/bitcoin/bitcoin.conf -datadir=${BITCOIN_DATA}
|
||
ExecStop=/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf stop
|
||
User=root
|
||
Group=root
|
||
Type=simple
|
||
Restart=on-failure
|
||
RuntimeDirectory=bitcoind
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
mkdir -p /etc/bitcoin
|
||
mkdir -p "${BITCOIN_DATA}"
|
||
|
||
# Bitcoin Core Configuration
|
||
cat > "$BITCOIN_CONF" <<EOF
|
||
server=1
|
||
txindex=0
|
||
rpcallowip=127.0.0.1
|
||
rpcbind=127.0.0.1
|
||
listen=1
|
||
daemon=0
|
||
prune=${PRUNE_SIZE}
|
||
rpcuser=${BTC_RPC_USER}
|
||
rpcpassword=${BTC_RPC_PASS}
|
||
rpcbind=127.0.0.1
|
||
maxconnections=250 # Allow up to 250 connections to speed up sync
|
||
maxuploadtarget=0 # Unlimited upload speed
|
||
dbcache=2048 # 2 GB RAM for sync
|
||
maxmempool=300 # Use less RAM post-sync
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable bitcoind
|
||
systemctl restart bitcoind
|
||
echo "[3/8] bitcoind started (pruned=550 MB)."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# [4/8] Fetch/build ckpool (solo) - official Bitbucket source
|
||
# ---------------------------------------------------------------------------
|
||
echo "[4/8] 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
|
||
fi
|
||
|
||
# extract
|
||
rm -rf ckpool
|
||
mkdir ckpool
|
||
tar -xzf "${CKPOOL_TGZ}" --strip-components=1 -C ckpool
|
||
|
||
cd ckpool
|
||
echo "[4/8] Building ckpool…"
|
||
./autogen.sh
|
||
CFLAGS="-O2" ./configure
|
||
make
|
||
|
||
# ensure the binary exists and is executable
|
||
if [[ ! -x "/opt/btc-solo/ckpool/src/ckpool" ]]; then
|
||
echo "ERROR: ckpool did not build correctly; /opt/btc-solo/ckpool/ckpool not found or not executable."
|
||
exit 1
|
||
fi
|
||
|
||
id -u "$CKPOOL_USER" >/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" <<EOF
|
||
{
|
||
"logdir" : "${APP_ROOT}/logs",
|
||
"stratum" : {
|
||
"port" : ${CKPOOL_PORT},
|
||
"bind" : "0.0.0.0"
|
||
},
|
||
"solomining" : true,
|
||
"coinbase-msg" : "Local Solo Mining",
|
||
"donaddress" : "1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP",
|
||
"donrate" : "2"
|
||
}
|
||
EOF
|
||
|
||
chown -R "$CKPOOL_USER":"$CKPOOL_USER" "$APP_ROOT"
|
||
|
||
# make ckpmsg available to PHP
|
||
ln -sf ${APP_ROOT}/${CKPOOL_USER}/src/ckpmsg /usr/local/bin/ckpmsg
|
||
|
||
cat > /etc/systemd/system/ckpool.service <<EOF
|
||
[Unit]
|
||
Description=ckpool solo mining server
|
||
After=network.target bitcoind.service
|
||
Wants=bitcoind.service
|
||
|
||
[Service]
|
||
User=${CKPOOL_USER}
|
||
Group=${CKPOOL_USER}
|
||
RuntimeDirectory=${CKPOOL_USER}
|
||
RuntimeDirectoryMode=0770
|
||
WorkingDirectory=${APP_ROOT}/${CKPOOL_USER}
|
||
ExecStart=${APP_ROOT}/${CKPOOL_USER}/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B -s /run/${CKPOOL_USER}
|
||
Restart=on-failure
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable ckpool
|
||
systemctl restart ckpool || true
|
||
echo "[4/8] ckpool service installed."
|
||
|
||
# ---------------------------------------------------------------------------
|
||
echo "[5/8] Helper scripts (sudo targets)"
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# apply admin creds
|
||
cat > /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 <adminuser> <adminpass>"
|
||
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 <size-in-mb|full>"
|
||
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 <donation-address> <donation-rate-percent>"
|
||
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 "[6/8] 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'
|
||
<?php
|
||
$flag = '/var/lib/btc-solo/.setup-complete';
|
||
if (!file_exists($flag)) {
|
||
header('Location: /activate/');
|
||
exit;
|
||
}
|
||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_ADDR'] ?? gethostname();
|
||
$host = preg_replace('/:\d+$/', '', $host);
|
||
$stratum = 'stratum+tcp://' . $host . ':3333';
|
||
?>
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Bitcoin Node for Solo Miners</title>
|
||
<link id="app-favicon" rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Crect width='64' height='64' fill='%23222'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='32' fill='%23ffb347'%3E%E2%9A%A0%3C/text%3E%3C/svg%3E">
|
||
<style>
|
||
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
|
||
header { background: #222; padding: 1rem 2rem; display:flex; justify-content:space-between; align-items:center; }
|
||
h1 { margin: 0; }
|
||
a { color: #7ad; text-decoration: none; }
|
||
.card { background: #1b1b1b; margin: 1rem 2rem; padding: 1rem 1.5rem; border-radius: 12px; }
|
||
code { background: #333; padding: 0.25rem 0.5rem; border-radius: 6px; }
|
||
.status-ok { color: #8f8; }
|
||
.status-warn { color: #ffb347; }
|
||
.status-err { color: #ff6b6b; }
|
||
.progress-wrap { background:#000; border-radius:6px; height:18px; overflow:hidden; }
|
||
.progress-bar { background:#4caf50; height:100%; width:0%; transition:width .4s ease; }
|
||
ul.status-list { list-style:none; padding-left:0; }
|
||
ul.status-list li { margin-bottom:.25rem; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div>
|
||
<h1>Bitcoin Node for Solo Miners</h1>
|
||
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
|
||
</div>
|
||
<div>
|
||
<a href="/settings.php">Settings</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="card" id="node-status">
|
||
<h2>Status</h2>
|
||
<p>Loading status…</p>
|
||
</div>
|
||
|
||
<div class="card" id="workers">
|
||
<h2>Workers</h2>
|
||
<p>No workers detected yet. This is normal while Bitcoin Core is syncing or if no ASICs are pointed at this server.</p>
|
||
</div>
|
||
|
||
<script>
|
||
// per-tab tracker with sliding window
|
||
const syncTracker = { samples: [] };
|
||
const WINDOW_SECONDS = 900; // 15 minutes
|
||
|
||
function formatEta(seconds) {
|
||
if (seconds == null || !isFinite(seconds)) return 'estimating…';
|
||
if (seconds < 60) return seconds.toFixed(0) + 's';
|
||
const mins = seconds / 60;
|
||
if (mins < 60) return mins.toFixed(1) + 'm';
|
||
const hours = mins / 60;
|
||
return hours.toFixed(1) + 'h';
|
||
}
|
||
|
||
function updateSamples(progress) {
|
||
const now = Date.now() / 1000;
|
||
syncTracker.samples.push({ t: now, p: progress });
|
||
const cutoff = now - WINDOW_SECONDS;
|
||
syncTracker.samples = syncTracker.samples.filter(s => s.t >= cutoff);
|
||
}
|
||
|
||
function estimateEta(progress) {
|
||
const samples = syncTracker.samples;
|
||
if (samples.length < 2) return null;
|
||
const first = samples[0];
|
||
const last = samples[samples.length - 1];
|
||
const dP = last.p - first.p;
|
||
const dT = last.t - first.t;
|
||
if (dP <= 0 || dT <= 5) return null;
|
||
const rate = dP / dT; // % per second
|
||
if (rate <= 0) return null;
|
||
const remaining = Math.max(0, 100 - progress);
|
||
return remaining / rate;
|
||
}
|
||
|
||
function renderServices(svcs) {
|
||
if (!svcs) return '';
|
||
const map = {
|
||
bitcoind: 'Bitcoin Core',
|
||
ckpool: 'CKPool'
|
||
};
|
||
let out = '<h3>Service Health</h3><ul class="status-list">';
|
||
for (const key of Object.keys(map)) {
|
||
if (!(key in svcs)) continue;
|
||
const state = svcs[key];
|
||
const ok = state === 'active';
|
||
out += '<li>' + (ok ? '✅' : '❌') + ' ' + map[key] + ' — ' + state + '</li>';
|
||
}
|
||
out += '</ul>';
|
||
return out;
|
||
}
|
||
|
||
async function loadStatus() {
|
||
try {
|
||
const res = await fetch('/status.php', {cache: 'no-store'});
|
||
const data = await res.json();
|
||
const box = document.getElementById('node-status');
|
||
|
||
if (data.error) {
|
||
box.innerHTML = '<h2>Status</h2><p class="status-err">' + data.error + '</p>' + renderServices(data.services);
|
||
return;
|
||
}
|
||
|
||
const ibd = !!data.initialblockdownload;
|
||
const blocks = data.blocks ?? '–';
|
||
const pruned = data.pruned ? '(pruned)' : '';
|
||
let progress = 0;
|
||
if (typeof data.verificationprogress === 'number') {
|
||
progress = data.verificationprogress * 100;
|
||
} else if (data.blocks && data.headers && data.headers > 0) {
|
||
progress = (data.blocks / data.headers) * 100;
|
||
}
|
||
|
||
// Update favicon dynamically
|
||
setFaviconAndTitle(ibd, progress);
|
||
|
||
// update window + estimate
|
||
updateSamples(progress);
|
||
const etaSeconds = ibd ? estimateEta(progress) : null;
|
||
|
||
let html = '<h2>Status</h2>';
|
||
|
||
if (ibd) {
|
||
html += '<p class="status-warn">⚠ Bitcoin Core is syncing. Your miners will not be able to submit shares or receive work until the node is fully synced.</p>';
|
||
} else {
|
||
html += '<p class="status-ok">✔ Bitcoin Core is in sync.</p>';
|
||
}
|
||
|
||
html += '<p>Blocks: ' + blocks + ' ' + pruned + '</p>';
|
||
html += '<div class="progress-wrap"><div class="progress-bar" id="syncbar"></div></div>';
|
||
html += '<p><small>Sync progress: ' + progress.toFixed(1) + '%';
|
||
if (ibd) {
|
||
if (etaSeconds && isFinite(etaSeconds) && etaSeconds > 0) {
|
||
const etaDate = new Date(Date.now() + etaSeconds * 1000);
|
||
const etaString = etaDate.toLocaleString(undefined, {
|
||
month: 'long',
|
||
day: 'numeric',
|
||
year: 'numeric',
|
||
hour: 'numeric',
|
||
minute: '2-digit'
|
||
});
|
||
html += ' • Estimated time left: ' + formatEta(etaSeconds) + ' (' + etaString + ')';
|
||
}
|
||
}
|
||
html += '</small></p>';
|
||
|
||
// append service health
|
||
html += renderServices(data.services);
|
||
|
||
box.innerHTML = html;
|
||
|
||
const bar = document.getElementById('syncbar');
|
||
if (bar) {
|
||
bar.style.width = Math.min(progress, 100).toFixed(2) + '%';
|
||
}
|
||
|
||
} catch (e) {
|
||
const box = document.getElementById('node-status');
|
||
box.innerHTML = '<h2>Status</h2><p class="status-err">Failed to fetch status.</p>';
|
||
}
|
||
}
|
||
|
||
// Inline SVG favicons
|
||
const FAV_SYNC = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Crect width='64' height='64' fill='%23222'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='32' fill='%23ffb347'%3E%E2%9A%A0%3C/text%3E%3C/svg%3E";
|
||
const FAV_READY = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='64' height='64'%3E%3Crect width='64' height='64' fill='%23161717'/%3E%3Ctext x='50%25' y='55%25' text-anchor='middle' font-size='34' fill='%238f8'%3E%E2%9C%94%3C/text%3E%3C/svg%3E";
|
||
|
||
function setFaviconAndTitle(isSyncing, progress) {
|
||
const link = document.getElementById('app-favicon');
|
||
if (!link) return;
|
||
if (isSyncing) {
|
||
link.href = FAV_SYNC;
|
||
document.title = "Syncing… " + progress.toFixed(1) + "% | Bitcoin Node";
|
||
} else {
|
||
link.href = FAV_READY;
|
||
document.title = "Ready • Bitcoin Node";
|
||
}
|
||
}
|
||
|
||
loadStatus();
|
||
setInterval(loadStatus, 5000);
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|
||
EOF
|
||
|
||
# Settings page
|
||
cat > "$WWW_ROOT/settings.php" <<'EOF'
|
||
<?php
|
||
$flag = '/var/lib/btc-solo/.setup-complete';
|
||
if (!file_exists($flag)) {
|
||
header('Location: /activate/');
|
||
exit;
|
||
}
|
||
|
||
$opts = [
|
||
'550' => '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';
|
||
?>
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Bitcoin Node for Solo Miners [Settings]</title>
|
||
<style>
|
||
body { font-family: sans-serif; background: #111; color: #eee; margin: 0; }
|
||
header { background: #222; padding: 1rem 2rem; }
|
||
h1 { margin: 0; }
|
||
.card { background: #1b1b1b; margin: 1rem 2rem; padding: 1rem 1.5rem; border-radius: 12px; }
|
||
code { background: #333; padding: 0.25rem 0.5rem; border-radius: 6px; }
|
||
label { display:block; margin-bottom: .5rem; }
|
||
select, button { padding: .4rem .6rem; border-radius: 6px; border:0; }
|
||
button { background:#4caf50; color:#fff; cursor:pointer; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>Bitcoin Node for Solo Miners</h1>
|
||
<p style="margin:0;">A Linux Appliance by Robbie Ferguson</p>
|
||
</header>
|
||
<div class="card">
|
||
<h2>Stratum Endpoint</h2>
|
||
<p><code><?php echo htmlspecialchars($stratum); ?></code></p>
|
||
<p>Username: BTC address for payout</p>
|
||
<p>Password: anything</p>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Blockchain Prune Size</h2>
|
||
<form method="post">
|
||
<label for="prune">Current setting:</label>
|
||
<select name="prune" id="prune">
|
||
<?php foreach ($opts as $val => $label): ?>
|
||
<option value="<?php echo $val; ?>" <?php echo ($val == $current ? 'selected' : ''); ?>>
|
||
<?php echo $label; ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit">Apply</button>
|
||
<p><small><b>Note:</b> 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.</small></p>
|
||
<p><small>Changing this will restart bitcoind.</small></p>
|
||
</form>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Donate to Bald Nerd</h2>
|
||
<p><small>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.</small></p>
|
||
<?php
|
||
$conf = @file_get_contents('/opt/btc-solo/conf/ckpool.conf');
|
||
$currAddr = '1MoGAsK8bRFKjHrpnpFJyqxdqamSPH19dP';
|
||
$currRate = 2;
|
||
if ($conf) {
|
||
if (preg_match('/"donaddress"\s*:\s*"([^"]+)"/', $conf, $m)) $currAddr = $m[1];
|
||
if (preg_match('/"donrate"\s*:\s*([0-9.]+)/', $conf, $m)) $currRate = $m[1];
|
||
}
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['donation_addr'])) {
|
||
$newAddr = trim($_POST['donation_addr']);
|
||
$newRate = floatval($_POST['donation_rate']);
|
||
if ($newAddr !== '') {
|
||
shell_exec('sudo /usr/local/sbin/btc-set-donation.sh '
|
||
. escapeshellarg($newAddr) . ' ' . escapeshellarg($newRate));
|
||
$currAddr = $newAddr;
|
||
$currRate = $newRate;
|
||
}
|
||
}
|
||
?>
|
||
<form method="post">
|
||
<label>Donation address (payout for your 2%)</label>
|
||
<input type="text" name="donation_addr" value="<?php echo htmlspecialchars($currAddr); ?>" style="width:100%;max-width:420px;">
|
||
<label>Donation %</label>
|
||
<select name="donation_rate">
|
||
<?php foreach ([0,0.5,1,2,3,4,5] as $r): ?>
|
||
<option value="<?php echo $r; ?>" <?php echo ($r == $currRate ? 'selected' : ''); ?>>
|
||
<?php echo $r; ?>%
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<button type="submit">Save</button>
|
||
</form>
|
||
</div>
|
||
</body>
|
||
</html>
|
||
EOF
|
||
|
||
# AJAX status info
|
||
cat > "$WWW_ROOT/status.php" <<'EOF'
|
||
<?php
|
||
// lightweight JSON status for dashboard / monitoring
|
||
|
||
$flag = '/var/lib/btc-solo/.setup-complete';
|
||
if (!file_exists($flag)) {
|
||
http_response_code(403);
|
||
header('Content-Type: application/json');
|
||
echo json_encode(['error' => '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
|
||
$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];
|
||
}
|
||
}
|
||
|
||
// ---- 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,
|
||
'used_bytes' => $btc_dir_size,
|
||
],
|
||
],
|
||
], JSON_UNESCAPED_SLASHES);
|
||
EOF
|
||
|
||
# activation page
|
||
cat > "$WWW_ROOT/activate/index.php" <<'EOF'
|
||
<?php
|
||
$flag = '/var/lib/btc-solo/.setup-complete';
|
||
if (file_exists($flag)) {
|
||
http_response_code(403);
|
||
echo "Setup already completed.";
|
||
exit;
|
||
}
|
||
|
||
$msg = '';
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$adminuser = trim($_POST['adminuser'] ?? '');
|
||
$adminpass = trim($_POST['adminpass'] ?? '');
|
||
$regen = isset($_POST['regen_cert']);
|
||
|
||
if ($adminuser !== '' && $adminpass !== '') {
|
||
// apply admin creds (writes rpcuser/rpcpassword, restarts services)
|
||
shell_exec('sudo /usr/local/sbin/btc-apply-admin.sh ' . escapeshellarg($adminuser) . ' ' . escapeshellarg($adminpass));
|
||
// optionally regenerate HTTPS cert
|
||
if ($regen) {
|
||
shell_exec('sudo /usr/local/sbin/btc-make-cert.sh');
|
||
}
|
||
// create setup-complete flag
|
||
file_put_contents($flag, date('c'));
|
||
// redirect to main dashboard
|
||
header('Location: /');
|
||
exit;
|
||
} else {
|
||
$msg = 'Both fields are required.';
|
||
}
|
||
}
|
||
?>
|
||
<!doctype html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Create New Admin User</title>
|
||
<style>
|
||
body { font-family: sans-serif; background: #111; color: #eee; display:flex; justify-content:center; align-items:center; height:100vh; }
|
||
form { background: #1b1b1b; padding: 2rem; border-radius: 12px; width: 340px; }
|
||
label { display:block; margin-bottom: .5rem; }
|
||
input[type=text], input[type=password] { width:100%; padding:.5rem; margin-bottom:1rem; border:0; border-radius:6px; }
|
||
button { padding:.5rem 1rem; border:0; border-radius:6px; background:#4caf50; color:#fff; cursor:pointer; }
|
||
.msg { color: #f66; margin-bottom:1rem; }
|
||
h2 { margin-top:0; }
|
||
small { color:#aaa; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<form method="post">
|
||
<h2>Create New Admin User</h2>
|
||
<p><small>
|
||
This sets the Bitcoin node’s administrative (RPC) credentials used to control the server itself.<br /><br />
|
||
Keep these private — they grant full access to your node.<br /><br />
|
||
These credentials are <b>not</b> used by miners to connect; miners only need your Bitcoin address and can use any password.
|
||
</small></p>
|
||
<?php if ($msg): ?>
|
||
<div class="msg"><?php echo htmlspecialchars($msg); ?></div>
|
||
<?php endif; ?>
|
||
<label>Admin Username</label>
|
||
<input type="text" name="adminuser" value="">
|
||
<label>Admin Password</label>
|
||
<input type="password" name="adminpass" value="">
|
||
<label style="display:flex;gap:.5rem;align-items:center;">
|
||
<input type="checkbox" name="regen_cert" checked>
|
||
Generate HTTPS certificate
|
||
</label>
|
||
<button type="submit">Continue</button>
|
||
</form>
|
||
</body>
|
||
</html>
|
||
EOF
|
||
|
||
# ---------------------------------------------------------------------------
|
||
echo "[7/8] 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 <<EOF
|
||
server {
|
||
listen 80 default_server;
|
||
return 301 https://\$host\$request_uri;
|
||
}
|
||
|
||
server {
|
||
listen 443 ssl;
|
||
ssl_certificate ${CERT_DIR}/btc-solo.crt;
|
||
ssl_certificate_key ${CERT_DIR}/btc-solo.key;
|
||
|
||
root ${WWW_ROOT};
|
||
index index.php;
|
||
|
||
# main app: try file, dir, then index.php
|
||
location / {
|
||
try_files \$uri \$uri/ /index.php;
|
||
}
|
||
|
||
# activation: let PHP regex still handle .php
|
||
location /activate/ {
|
||
try_files \$uri /activate/index.php;
|
||
}
|
||
|
||
# PHP handler (use the generic symlink, which you have)
|
||
location ~ \.php\$ {
|
||
include snippets/fastcgi-php.conf;
|
||
fastcgi_pass unix:/run/php/php-fpm.sock;
|
||
}
|
||
}
|
||
EOF
|
||
|
||
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 "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
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
echo "[8/8] Done"
|
||
# ---------------------------------------------------------------------------
|
||
echo
|
||
echo "================================================================"
|
||
echo " Bitcoin Solo Miner Appliance installed."
|
||
echo
|
||
echo " 1) Open: https://$(hostname -I | awk '{print $1}')/activate/"
|
||
echo " 2) Set Admin Username / Admin Password"
|
||
echo " 3) (Optional) Regenerate the cert"
|
||
echo " 4) You will be redirected to the dashboard."
|
||
echo
|
||
echo " Stratum endpoint for ASICs: stratum+tcp://$(hostname -I | awk '{print $1}'):${CKPOOL_PORT}"
|
||
echo "================================================================"
|