git / moonbite-network / release/miner/mine.ps1 · raw

# MoonBite solo miner - Windows (PowerShell).## Solo and pool-free: runs a full MoonBite node on your PC and mines straight to# YOUR wallet address. No pool, no account, no middleman.##   .\mine.ps1 moon1youraddress   mine rewards to your wallet#   .\mine.ps1                     confirm the saved address (Enter keeps it), or ask for one#   .\mine.ps1 address             print the reward address in use#   .\mine.ps1 stop                stop the node#   .\mine.ps1 -Cores 50%          mine with half your CPU cores (or -Cores 4);#                                  default is every physical core. The same#                                  setting works as $env:MOONBITE_CORES.## Get your address from the MoonBite wallet app (or moonbite.org/wallet):# create a wallet, open Receive, copy the moon1... address.param([string]$arg = "", [string]$Cores = "")$ErrorActionPreference = "Stop"$here    = Split-Path -Parent $MyInvocation.MyCommand.Path$daemon  = Join-Path $here "moonbited.exe"$clibin  = Join-Path $here "moonbite-cli.exe"if (-not (Test-Path $daemon)) { Write-Error "moonbited.exe not found next to this script."; exit 1 }# What this script will never do (docs/ADR-013-miner-distribution-policy.md):# download or execute anything, add a startup entry / scheduled task / service,# request administrator rights, or change Windows Defender or any other# security setting. It runs the node binary that sits next to it, and only# while this window is open.function Show-BinaryHash {  # Print the SHA-256 of the exact binaries about to run, so the user can  # compare them against the published manifest before mining starts.  foreach ($bin in @($daemon, $clibin)) {    if (Test-Path $bin) {      $h = (Get-FileHash -Algorithm SHA256 -Path $bin).Hash.ToLower()      Write-Host (" sha256 {0}  {1}" -f $h, (Split-Path -Leaf $bin))    }  }  Write-Host " Compare against SHA256SUMS.txt from the release page:"  Write-Host "   https://moonbite.org/mine"  Write-Host " (see ANTIVIRUS.md in this folder for signature + attestation checks)"}$datadir = if ($env:MOONBITE_DATADIR) { $env:MOONBITE_DATADIR } else { Join-Path $env:USERPROFILE ".moonbite" }$conf    = Join-Path $datadir "moonbite.conf"$rewardFile = Join-Path $datadir "reward-address.txt"function Invoke-Cli {  # moonbite-cli is a native exe: a non-zero exit does not raise a PowerShell  # error, so probe $LASTEXITCODE and throw ourselves. Without this every  # "is the node up?" check silently succeeds and the node is never started.  $out = & $clibin "-datadir=$datadir" "-conf=$conf" @args 2>&1  if ($LASTEXITCODE -ne 0) { throw ("moonbite-cli failed: " + ($out | Out-String)) }  $out | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }}function Test-Addr([string]$a) { return ($a -match '^moon1[0-9a-z]{20,88}$') }function Get-PhysicalCores {  try { $n = (Get-CimInstance Win32_Processor -ErrorAction Stop | Measure-Object -Property NumberOfCores -Sum).Sum; if ($n -ge 1) { return [int]$n } } catch {}  return [Environment]::ProcessorCount}function Resolve-Threads([string]$want, [int]$phys) {  # How many CPU cores to mine with. 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.  if (-not $want) { $want = $env:MOONBITE_CORES }  if (-not $want) { return $phys }  $w = "$want".Trim()  if ($w -match '^(\d{1,3})%$') { $n = [math]::Round($phys * [int]$Matches[1] / 100) }  elseif ($w -match '^\d{1,4}$') { $n = [int]$w }  else { throw "-Cores must be a number of cores (e.g. 4) or a share of them (e.g. 50%), not '$want'." }  return [int][math]::Max(1, [math]::Min($phys, $n))}$physCores = Get-PhysicalCores$threads   = Resolve-Threads $Cores $physCoresfunction Get-P2pPort { if ($env:MOONBITE_P2P_PORT) { [int]$env:MOONBITE_P2P_PORT } else { 9444 } }function Get-RpcPort { if ($env:MOONBITE_RPC_PORT) { [int]$env:MOONBITE_RPC_PORT } else { 9445 } }function Test-PortListening([int]$port) {  # Get-NetTCPConnection is missing on very old Windows; fall back to netstat.  try { return [bool](Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction Stop) }  catch { return [bool](netstat -ano | Select-String ":$port\s.*LISTENING") }}function Write-Conf {  New-Item -ItemType Directory -Force -Path $datadir | Out-Null  $pw = $null  if (Test-Path $conf) { $pw = (Select-String -Path $conf -Pattern '^rpcpassword=(.*)$').Matches.Groups[1].Value }  if (-not $pw) {    # Cryptographic RNG for the node RPC password. Get-Random is NOT a CSPRNG    # (seedable/predictable), so a local attacker could guess it and drive the    # node's wallet over 127.0.0.1 RPC. Use RandomNumberGenerator for 24 bytes    # (48 hex chars).    $bytes = New-Object byte[] 24    [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes)    $pw = -join ($bytes | ForEach-Object { '{0:x2}' -f $_ })  }  $p2p = if ($env:MOONBITE_P2P_PORT) { "port=$($env:MOONBITE_P2P_PORT)`n" } else { "" }  $rpcp = if ($env:MOONBITE_RPC_PORT) { "rpcport=$($env:MOONBITE_RPC_PORT)`n" } else { "" }  @"server=1listen=1dbcache=512rpcuser=moonminerrpcpassword=$pwminingthreads=$threads$p2p${rpcp}addnode=67.205.154.64:9444addnode=165.232.106.9:9444"@ | Set-Content -Encoding ascii $conf}function Wait-Rpc {  Write-Host " Starting the node..."  for ($i=0; $i -lt 90; $i++) {    try { Invoke-Cli getblockcount | Out-Null; return } catch {}    if ($i -eq 4) { Write-Host " Still starting - the first run builds the RandomX cache, which can take a minute..." }    Start-Sleep 2  }  throw "The node did not start. Check $datadir\debug.log for the reason."}function Start-Node {  # Already have a reachable node (e.g. this script re-run)? Use it. Its core  # count was fixed when it started, so say how to apply a different one.  $up = $false; try { Invoke-Cli getblockcount | Out-Null; $up = $true } catch {}  if ($up) {    $cur = $null    if (Test-Path $conf) { $hit = Select-String -Path $conf -Pattern '^miningthreads=(\d+)$'; if ($hit) { $cur = [int]$hit.Matches[0].Groups[1].Value } }    if ($cur -ne $threads) { Write-Host " Note: the node is already running with a different core setting. To mine with $threads cores, run .\mine.ps1 stop and start again." }    return  }  # A node we can't authenticate to may already own the ports - typically a  # second copy of this miner on the same PC. Only one node runs per machine;  # launching another just fails to bind and hangs. Say so plainly and stop.  if ((Test-PortListening (Get-P2pPort)) -or (Test-PortListening (Get-RpcPort))) {    Write-Host ""    Write-Host "  A MoonBite node is already running on this computer"    Write-Host "  (network port $(Get-P2pPort) is in use). You are already mining -"    Write-Host "  only one node runs per machine, so this window isn't needed."    Write-Host ""    Write-Host "  To run a SECOND, separate miner here on purpose, give it its own"    Write-Host "  ports and data folder first, for example:"    Write-Host '      $env:MOONBITE_P2P_PORT=19444; $env:MOONBITE_RPC_PORT=19445'    Write-Host '      $env:MOONBITE_DATADIR="$env:USERPROFILE\.moonbite2"'    Write-Host "      .\mine.ps1"    Write-Host ""    exit 0  }  Write-Conf  Start-Process -FilePath $daemon -ArgumentList "-datadir=$datadir","-conf=$conf" -WindowStyle Hidden | Out-Null  Wait-Rpc}function Test-AddrValid([string]$a) {  # Shape check, then the node's own bech32 checksum check, so a mistyped  # address is refused instead of silently paying a stranger.  if (-not (Test-Addr $a)) { return $false }  try { return ((Invoke-Cli validateaddress $a | Out-String | ConvertFrom-Json).isvalid -eq $true) } catch { return $false }}function Resolve-Reward([string]$cand, [switch]$Confirm) {  # Priority: CLI arg, env var, a previously saved address. When mining starts  # (-Confirm) with a saved address and a person at the keyboard, 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.  if ($cand) {    if (-not (Test-AddrValid $cand)) { throw "'$cand' is not a valid MoonBite address. Check it and try again." }    Set-Content -Encoding ascii $rewardFile $cand; return $cand  }  if ($env:MOONBITE_ADDRESS -and (Test-AddrValid $env:MOONBITE_ADDRESS)) { Set-Content -Encoding ascii $rewardFile $env:MOONBITE_ADDRESS; return $env:MOONBITE_ADDRESS }  if (Test-Path $rewardFile) {    $a = (Get-Content $rewardFile -Raw).Trim()    if (Test-Addr $a) {      if ($Confirm -and [Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {        Write-Host ""        Write-Host " Rewards go to: $a"        Write-Host " Press Enter to keep it, or paste a different address."        while ($true) {          $in = "$(Read-Host ' Address [Enter = keep]')".Trim()          if (-not $in) { break }          if (Test-AddrValid $in) { Set-Content -Encoding ascii $rewardFile $in; Write-Host " Saved. Rewards now go to $in"; return $in }          Write-Host " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to keep the saved one."        }      }      return $a    }  }  if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) {    Write-Host ""    Write-Host " Where should your mining rewards go?"    Write-Host " Paste your wallet address (MoonBite wallet app > Receive, starts with moon1),"    Write-Host " or just press Enter to create a wallet on this computer instead."    while ($true) {      $in = (Read-Host " Address").Trim()      if (-not $in) { break }      if (Test-AddrValid $in) { Set-Content -Encoding ascii $rewardFile $in; Write-Host " Saved. Next start just press Enter to keep it."; return $in }      Write-Host " That isn't a valid MoonBite address (typo?). Paste it again, or press Enter to skip."    }  }  try { Invoke-Cli createwallet "miner" | Out-Null } catch { try { Invoke-Cli loadwallet "miner" | Out-Null } catch {} }  $a = (Invoke-Cli -rpcwallet=miner getnewaddress "mining" | Out-String).Trim()  if (-not (Test-Addr $a)) { throw "Could not create a mining wallet on this node." }  Set-Content -Encoding ascii $rewardFile $a  Write-Host ""  Write-Host " Rewards will go to a wallet on THIS computer only: $a"  Write-Host " IMPORTANT: it has no backup and no password yet. If this disk dies, the coins are gone."  Write-Host "   Back it up, then copy the file to a USB stick:"  Write-Host "   .\moonbite-cli.exe -datadir=`"$datadir`" -conf=`"$conf`" -rpcwallet=miner backupwallet `"$env:USERPROFILE\Desktop\moonbite-miner-wallet.dat`""  Write-Host "   Or switch to your wallet app any time:  .\mine.ps1 moon1youraddress"  Write-Host ""  return $a}switch ($arg) {  "stop" {    Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*moonbite-cli*generatetoaddress*" } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue }    try { Invoke-Cli stop } catch {}    for ($i=0; $i -lt 20; $i++) { if (-not (Get-Process moonbited -ErrorAction SilentlyContinue)) { break }; Start-Sleep 2 }    Get-Process moonbited -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue    Write-Host "stopped."  }  "address" { Show-BinaryHash; Start-Node; Write-Host "Rewards go to: $(Resolve-Reward '')" }  default {    Show-BinaryHash    Start-Node    $addr = Resolve-Reward $arg -Confirm    Write-Host "======================================================================"    Write-Host " MoonBite solo miner"    Write-Host " Rewards to: $addr"    Write-Host " Using $threads of $physCores CPU cores (change with -Cores 50% or -Cores 4)."    Write-Host " Ctrl-C to stop. Coins are spendable 100 blocks after being mined."    Write-Host "======================================================================"    Write-Host " Syncing with the network before mining..."    $stable = 0    while ($true) {      try {        $info = Invoke-Cli getblockchaininfo | Out-String | ConvertFrom-Json        if ($info -and -not $info.initialblockdownload -and $info.blocks -ge $info.headers) { 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 = [int]("$(Invoke-Cli getconnectioncount)".Trim())        if ($info -and $peers -ge 1 -and $info.blocks -ge $info.headers) { $stable++ } else { $stable = 0 }        if ($stable -ge 5) { Write-Host "   Up to date with $peers peer(s); the chain is just quiet."; break }        Write-Host "   ...$($info.blocks) / $($info.headers) blocks"      } catch {}      Start-Sleep 3    }    # 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.    try {      $gtime = [long]((Invoke-Cli getblockheader "$(Invoke-Cli getblockhash 0)".Trim() | Out-String | ConvertFrom-Json).time)      $opens = $gtime - 7200 + 5      if ([int]"$(Invoke-Cli getblockcount)".Trim() -eq 0 -and [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() -lt $opens) {        # Announced in Central European time (CET/CEST).        $cet = [TimeZoneInfo]::FindSystemTimeZoneById("Central European Standard Time")        $local = [TimeZoneInfo]::ConvertTimeFromUtc([DateTimeOffset]::FromUnixTimeSeconds($gtime - 7200).UtcDateTime, $cet)        $at = $local.ToString('yyyy-MM-dd HH:mm') + $(if ($cet.IsDaylightSavingTime($local)) { " CEST" } else { " CET" })        Write-Host " MoonBite opens for mining at $at. Nobody can mine before then."        Write-Host " Leave this window open: mining starts by itself at launch."        while (($left = $opens - [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()) -gt 0) {          $s = [TimeSpan]::FromSeconds($left)          Write-Host ("   launch in {0}d {1:00}h {2:00}m {3:00}s" -f $s.Days, $s.Hours, $s.Minutes, $s.Seconds)          Start-Sleep ([Math]::Max(1, [Math]::Min($left, $(if ($left -gt 3600) { 600 } elseif ($left -gt 120) { 60 } else { 10 }))))        }        Write-Host " Launch! Mining is open."      }    } catch {}    Write-Host " Synced at height $(Invoke-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    # $roundSecs 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.    $roundSecs = 20    $tries = if ($env:MAXTRIES) { [int]$env:MAXTRIES } else { 5000 }    while ($true) {      try {        $out = ""        $sw = [Diagnostics.Stopwatch]::StartNew()        try { $out = (Invoke-Cli generatetoaddress 1 $addr $tries | Out-String) } catch {}        $secs = $sw.Elapsed.TotalSeconds        if ($out -match '[0-9a-f]{64}') { $found++; Write-Host "  BLOCK FOUND!  height $(Invoke-Cli getblockcount)   (found $found this session)   peers $(Invoke-Cli getconnectioncount)" }        elseif (-not $env:MAXTRIES -and $secs -gt 1) { $tries = [int][Math]::Min(200000, [Math]::Max(2000, $tries / $secs * $roundSecs)) }        if ($secs -lt 1) { Start-Sleep 1 }      } catch {        Write-Host "  (rpc hiccup) retrying..."; Start-Sleep 5      }    }  }}