#!/usr/bin/env bash # MoonBite solo miner - Linux & macOS. # # Solo and pool-free: this runs a full MoonBite node on your machine and mines # straight to YOUR wallet address. No pool, no account, no middleman. # # ./mine.sh moon1yourwalletaddress mine rewards to your wallet # ./mine.sh confirm the saved address (Enter keeps it), or ask for one # ./mine.sh --cores 50% [address] mine with half your CPU cores (or --cores 4); # default is every physical core (also MOONBITE_CORES) # ./mine.sh address print the reward address in use # ./mine.sh stop stop the node # # Get your address from the MoonBite wallet app (or moonbite.org/wallet): # create a wallet, open Receive, and copy the moon1... address. set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DAEMON="$HERE/moonbited"; [ -x "$DAEMON" ] || DAEMON="$(command -v moonbited || true)" CLIBIN="$HERE/moonbite-cli"; [ -x "$CLIBIN" ] || CLIBIN="$(command -v moonbite-cli || true)" [ -x "$DAEMON" ] || { echo "moonbited not found next to this script or on PATH." >&2; exit 1; } # What this script will never do (docs/ADR-013-miner-distribution-policy.md): # download or execute anything, install a service / cron / login item, ask for # sudo, or change any security setting. It runs the node binary next to it (or # on PATH), only for as long as you leave it running. show_binary_hash() { # Print the SHA-256 of the exact binaries about to run so the user can # compare them against the published manifest before mining starts. local tool="" if command -v sha256sum >/dev/null 2>&1; then tool="sha256sum"; elif command -v shasum >/dev/null 2>&1; then tool="shasum -a 256"; fi if [ -n "$tool" ]; then local b h for b in "$DAEMON" "$CLIBIN"; do [ -x "$b" ] || continue h=$($tool "$b" 2>/dev/null | awk '{print $1}') echo " sha256 $h $(basename "$b")" done fi echo " Compare against SHA256SUMS.txt from the release page:" echo " https://moonbite.org/mine" echo " (see ANTIVIRUS.md next to this script for signature + attestation checks)" } # --cores N or --cores N% (or MOONBITE_CORES): how many CPU cores to mine with. if [ "${1:-}" = "--cores" ]; then [ -n "${2:-}" ] || { echo "--cores needs a value, e.g. --cores 4 or --cores 50%" >&2; exit 1; } MOONBITE_CORES="$2"; shift 2 fi physical_cores() { local n="" if command -v lscpu >/dev/null 2>&1; then n=$(lscpu -p 2>/dev/null | grep -v '^#' | cut -d, -f2,3 | sort -u | wc -l); fi [ "${n:-0}" -ge 1 ] 2>/dev/null || n=$(sysctl -n hw.physicalcpu 2>/dev/null || true) [ "${n:-0}" -ge 1 ] 2>/dev/null || n=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 1) echo "$n" } # Default: every physical core. RandomX gains nothing from hyperthreads (each VM # wants its own 2 MB of L3) and runs slower on them, so requests above the # physical count are capped there. Accepts a count ("4") or a share ("50%"). resolve_threads() { local phys="$1" want="${MOONBITE_CORES:-}" n [ -z "$want" ] && { echo "$phys"; return; } case "$want" in *%) n="${want%\%}"; case "$n" in ''|*[!0-9]*) echo bad; return;; esac; n=$(( (phys * n + 50) / 100 ));; ''|*[!0-9]*) echo bad; return;; *) n="$want";; esac [ "$n" -lt 1 ] && n=1 [ "$n" -gt "$phys" ] && n="$phys" echo "$n" } PHYS=$(physical_cores) THREADS=$(resolve_threads "$PHYS") [ "$THREADS" = bad ] && { echo "--cores must be a number of cores (e.g. 4) or a share of them (e.g. 50%), not '${MOONBITE_CORES}'." >&2; exit 1; } DATADIR="${MOONBITE_DATADIR:-$HOME/.moonbite}" CONF="$DATADIR/moonbite.conf" REWARD_FILE="$DATADIR/reward-address.txt" cli() { "$CLIBIN" -datadir="$DATADIR" -conf="$CONF" "$@"; } node_pid() { pgrep -f "moonbited -datadir=$DATADIR" 2>/dev/null | head -1; } is_addr() { case "$1" in moon1[0-9a-z]*) [ ${#1} -ge 26 ] && [ ${#1} -le 90 ];; *) return 1;; esac; } write_conf() { mkdir -p "$DATADIR" local pw; pw=$(sed -n 's/^rpcpassword=//p' "$CONF" 2>/dev/null | head -1) || true [ -z "$pw" ] && pw=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n') # MOONBITE_P2P_PORT / MOONBITE_RPC_PORT let more than one node run on the # same machine at once (mine.ps1 already supports this) - the default ports # collide the moment a second instance, e.g. a seed node, is already up. local p2p="" rpcp="" [ -n "${MOONBITE_P2P_PORT:-}" ] && p2p="port=$MOONBITE_P2P_PORT"$'\n' [ -n "${MOONBITE_RPC_PORT:-}" ] && rpcp="rpcport=$MOONBITE_RPC_PORT"$'\n' # Create the file 0600 from birth. A bare `>` here writes at the process # umask (often 0644), leaving the RPC password world-readable until the # chmod below runs - tighten umask around the write only, then restore it. local old_umask; old_umask=$(umask); umask 077 cat > "$CONF" </dev/null 2>&1 && return 0; sleep 2; done echo "node did not start (check $DATADIR/debug.log)" >&2; return 1 } # True if something is already listening on the given TCP port (ss, then # netstat, then /proc as fallbacks - one of them exists on any Linux/macOS). port_in_use() { local p="$1" if command -v ss >/dev/null 2>&1; then ss -ltn 2>/dev/null | grep -qE "[:.]$p\b" && return 0; fi if command -v netstat >/dev/null 2>&1; then netstat -an 2>/dev/null | grep -qE "[:.]$p\b.*LISTEN" && return 0; fi if command -v lsof >/dev/null 2>&1; then lsof -iTCP:"$p" -sTCP:LISTEN >/dev/null 2>&1 && return 0; fi return 1 } start_node() { # Already have a reachable node? Use it. Its core count was fixed when it # started, so say how to apply a different one. if cli getblockcount >/dev/null 2>&1; then local cur; cur=$(sed -n 's/^miningthreads=//p' "$CONF" 2>/dev/null | head -1) [ "$cur" != "$THREADS" ] && echo " Note: the node is already running with a different core setting. To mine with $THREADS cores, run ./mine.sh stop and start again." >&2 return 0 fi # A node we can't authenticate to may already own the ports - usually a # second copy of this miner on the same machine. Only one node runs per # machine; another just fails to bind and hangs. Say so and stop cleanly. local p2p="${MOONBITE_P2P_PORT:-9444}" rpcp="${MOONBITE_RPC_PORT:-9445}" if port_in_use "$p2p" || port_in_use "$rpcp"; then cat >&2 </dev/null wait_rpc } # Shape check, then the node's own bech32 checksum check, so a mistyped # address is refused instead of silently paying a stranger. addr_valid() { is_addr "$1" && cli validateaddress "$1" 2>/dev/null | grep -q '"isvalid": true'; } # Where mining rewards go. Priority: CLI arg, env, saved file. When mining # starts with a saved address and a person at the terminal, show it and let # them press Enter to keep it or paste a new one. With no address at all, ask # for one (Enter skips). Unattended runs never wait: they use the saved # address, or fall back to the node's own built-in wallet so mining still # starts with zero setup. Messages go to stderr: stdout is the address, # captured by the caller. Pass "confirm" as $2 to enable the start-up check. resolve_reward_address() { local cand="${1:-}" confirm="${2:-}" a in if [ -n "$cand" ]; then addr_valid "$cand" || { echo "'$cand' is not a valid MoonBite address. Check it and try again." >&2; exit 1; } printf '%s' "$cand" > "$REWARD_FILE"; echo "$cand"; return fi if [ -n "${MOONBITE_ADDRESS:-}" ] && addr_valid "$MOONBITE_ADDRESS"; then printf '%s' "$MOONBITE_ADDRESS" > "$REWARD_FILE"; echo "$MOONBITE_ADDRESS"; return; fi if [ -s "$REWARD_FILE" ]; then a=$(cat "$REWARD_FILE") if is_addr "$a"; then if [ "$confirm" = confirm ] && [ -t 0 ]; then { echo ""; echo " Rewards go to: $a"; echo " Press Enter to keep it, or paste a different address."; } >&2 while :; do in=""; read -r -p " Address [Enter = keep]: " in || break in="$(printf '%s' "$in" | tr -d '[:space:]')" [ -z "$in" ] && break if addr_valid "$in"; then printf '%s' "$in" > "$REWARD_FILE"; echo " Saved. Rewards now go to $in" >&2; echo "$in"; return fi echo " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to keep the saved one." >&2 done fi echo "$a"; return fi fi if [ -t 0 ]; then { echo "" echo " Where should your mining rewards go?" echo " Paste your wallet address (MoonBite wallet app > Receive, starts with moon1)," echo " or just press Enter to create a wallet on this computer instead." } >&2 while :; do in=""; read -r -p " Address: " in || break in="$(printf '%s' "$in" | tr -d '[:space:]')" [ -z "$in" ] && break if addr_valid "$in"; then printf '%s' "$in" > "$REWARD_FILE"; echo " Saved. Next start just press Enter to keep it." >&2; echo "$in"; return fi echo " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to skip." >&2 done fi cli createwallet miner >/dev/null 2>&1 || cli loadwallet miner >/dev/null 2>&1 || true a=$(cli -rpcwallet=miner getnewaddress "mining"); printf '%s' "$a" > "$REWARD_FILE" { echo "" echo " Rewards will go to a wallet on THIS computer only: $a" echo " IMPORTANT: it has no backup and no password yet. If this disk dies, the coins are gone." echo " Back it up, then copy the file to a USB stick:" echo " $CLIBIN -datadir=\"$DATADIR\" -conf=\"$CONF\" -rpcwallet=miner backupwallet \"\$HOME/moonbite-miner-wallet.dat\"" echo " Or switch to your wallet app any time: ./mine.sh moon1youraddress" echo "" } >&2 echo "$a" } case "${1:-mine}" in address) show_binary_hash; start_node; echo "Rewards go to: $(resolve_reward_address)" ;; stop) pkill -f "moonbite-cli.*generatetoaddress" 2>/dev/null || true cli stop 2>/dev/null || true for _ in $(seq 1 20); do [ -z "$(node_pid)" ] && break; sleep 2; done p=$(node_pid); [ -n "$p" ] && kill "$p" 2>/dev/null || true for _ in $(seq 1 10); do [ -z "$(node_pid)" ] && break; sleep 1; done p=$(node_pid); [ -n "$p" ] && kill -9 "$p" 2>/dev/null || true echo "stopped." ;; mine|*) ARG=""; case "${1:-}" in moon1*) ARG="$1";; esac show_binary_hash start_node ADDR=$(resolve_reward_address "$ARG" confirm) echo "======================================================================" echo " MoonBite solo miner" echo " Rewards to: $ADDR" echo " Using $THREADS of $PHYS CPU cores (change with --cores 50% or --cores 4)." echo " Ctrl-C to stop. Coins are spendable 100 blocks after they are mined." echo "======================================================================" trap 'echo; echo "Miner stopped. Node still running - ./mine.sh stop to shut it down."; exit 0' INT echo " Syncing with the network before mining..." stable=0 while :; do ibd=$(cli getblockchaininfo 2>/dev/null | sed -n 's/.*"initialblockdownload": *\(true\|false\).*/\1/p' | head -1) bl=$(cli getblockcount 2>/dev/null || echo 0) hd=$(cli getblockchaininfo 2>/dev/null | sed -n 's/.*"headers": *\([0-9]*\).*/\1/p' | head -1) [ "$ibd" = "false" ] && [ -n "$hd" ] && [ "$bl" -ge "$hd" ] && break # A chain whose newest block is over a day old reports "initial download" # until someone mines the next block, so waiting on that flag alone # deadlocks a young or quiet chain. Holding every header our peers # announce, with peers connected, for ~15 s means nothing is left to fetch. peers=$(cli getconnectioncount 2>/dev/null || echo 0) if [ -n "$hd" ] && [ "${peers:-0}" -ge 1 ] && [ "$bl" -ge "$hd" ]; then stable=$((stable+1)); else stable=0; fi [ "$stable" -ge 5 ] && { echo " Up to date with $peers peer(s); the chain is just quiet."; break; } echo " ...$bl / ${hd:-?} blocks"; sleep 3 done # Public launch: the genesis block is stamped 2 h after the announced launch, # and no block may be more than 2 h ahead of the clock, so block 1 becomes # possible exactly at launch. Until then wait here instead of failing calls. gtime=$(cli getblockheader "$(cli getblockhash 0 2>/dev/null)" 2>/dev/null | sed -n 's/.*"time": *\([0-9]*\).*/\1/p' | head -1) if [ -n "$gtime" ] && [ "$(cli getblockcount 2>/dev/null || echo 1)" = "0" ]; then opens=$((gtime - 7200 + 5)) if [ "$(date -u +%s)" -lt "$opens" ]; then # Announced in Central European time (CET/CEST). at=$(TZ=Europe/Paris date -d "@$((gtime - 7200))" '+%Y-%m-%d %H:%M %Z' 2>/dev/null || TZ=Europe/Paris date -r "$((gtime - 7200))" '+%Y-%m-%d %H:%M %Z') echo " MoonBite opens for mining at $at. Nobody can mine before then." echo " Leave this running: mining starts by itself at launch." while left=$((opens - $(date -u +%s))); [ "$left" -gt 0 ]; do printf " launch in %dd %02dh %02dm %02ds\n" $((left/86400)) $((left%86400/3600)) $((left%3600/60)) $((left%60)) if [ "$left" -gt 3600 ]; then nap=600; elif [ "$left" -gt 120 ]; then nap=60; else nap=10; fi [ "$nap" -gt "$left" ] && nap=$left sleep "$nap" done echo " Launch! Mining is open." fi fi echo " Synced at height $(cli getblockcount). Mining now." FOUND=0 # The node hashes one block template per call and does not notice a newer # block from someone else until the call ends, so keep each call to about # ROUND_SECS of work: size the tries from the speed measured on the last # call that found nothing. Long calls waste work on a stale tip and orphan # blocks. MAXTRIES pins a fixed value instead. ROUND_SECS=20 TRIES="${MAXTRIES:-5000}" # Milliseconds; macOS date has no %N, so fall back to whole seconds there. now_ms() { local n; n=$(date +%s%N); case "$n" in *N) echo $(( $(date +%s) * 1000 ));; *) echo $(( n / 1000000 ));; esac; } while :; do T0=$(now_ms) OUT=$(cli generatetoaddress 1 "$ADDR" "$TRIES" 2>/dev/null || true) MS=$(( $(now_ms) - T0 )) if printf '%s' "$OUT" | grep -q '"[0-9a-f]\{64\}"'; then FOUND=$((FOUND+1)) echo " BLOCK FOUND! height $(cli getblockcount) (you have found $FOUND this session) peers $(cli getconnectioncount)" elif [ -z "${MAXTRIES:-}" ] && [ "$MS" -gt 1000 ]; then TRIES=$(( TRIES * ROUND_SECS * 1000 / MS )) [ "$TRIES" -lt 2000 ] && TRIES=2000 [ "$TRIES" -gt 200000 ] && TRIES=200000 fi [ "$MS" -lt 1000 ] && sleep 1 done ;; esac