Files
bitcoin-ckpool-appliance/installer

865 lines
26 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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"
# create the socket folder for CKPool so we can later parse the JSON output
mkdir -p /var/run/ckpool
chown ${CKPOOL_USER}:${CKPOOL_USER} /var/run/ckpool
# make ckpmsg available to PHP
ln -sf /opt/btc-solo/ckpool/src/ckpmsg /usr/local/bin/ckpmsg
cat > /etc/systemd/system/ckpool.service <<EOF
[Unit]
Description=ckpool solo mining server
After=network.target bitcoind.service
[Service]
User=${CKPOOL_USER}
Group=${CKPOOL_USER}
WorkingDirectory=${APP_ROOT}/ckpool
ExecStart=${APP_ROOT}/ckpool/src/ckpool -c ${APP_ROOT}/conf/ckpool.conf -B -s /var/run/ckpool
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>
<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 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(2) + '%';
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>';
}
}
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
// no session needed; this is polled often
$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;
}
// Bitcoin Core status
$cmd = '/usr/local/bin/bitcoin-cli -conf=/etc/bitcoin/bitcoin.conf getblockchaininfo 2>/dev/null';
$out = shell_exec($cmd);
$info = $out ? json_decode($out, true) : null;
// service health (simple is-active check)
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')
];
header('Content-Type: application/json');
if (!$info) {
echo json_encode([
'error' => 'bitcoin-cli failed',
'services' => $services
]);
exit;
}
echo json_encode([
'initialblockdownload' => $info['initialblockdownload'] ?? null,
'blocks' => $info['blocks'] ?? null,
'headers' => $info['headers'] ?? null,
'verificationprogress' => $info['verificationprogress'] ?? null,
'pruned' => $info['pruned'] ?? null,
'services' => $services
]);
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 nodes 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 "================================================================"