# zecnero-miner.ps1: the Zecnero Testnet POOL miner for Windows, native (no WSL). # A beta: the native Windows miner is new with testnet-v0.2.0; WSL stays the # tested path (install.ps1). # # Install: open Windows PowerShell (no need for "Run as administrator") and run # # irm https://downloads.zecnero.org/install-native.ps1 | iex # # or, where downloads.zecnero.org is slow or blocked, the same script from the # mirror on seed1: # # irm https://mirror.zecnero.org/install-native.ps1 | iex # # install-native.ps1 on the download server is this file, byte for byte. Run # through iex it installs; the copy it installs, bin\zecnero-miner.ps1, runs the # commands below through zecnero-miner.cmd beside it. # # This is POOL mining only: xmrig-zecnero mines on the Zecnero pool # (stratum.zecnero.org, TLS on 3334) and the pool pays a Sapling address. Solo # mining needs the node and the bridge, which run in Ubuntu on WSL: # # irm https://downloads.zecnero.org/install.ps1 | iex # # What the installer does: # 1. Reads LATEST (the current release folder), INSTALLERS.SHA256SUMS and its # signature from downloads.zecnero.org, or from mirror.zecnero.org when that # does not answer within about 10 seconds, and takes every file of the run # from that one host. INSTALLERS.SHA256SUMS lists the installers and LATEST # and is signed with the release key (the list install.ps1 checks too): its # signature is checked as in step 2, then LATEST against it, and, run as # the one-liner or as a downloaded file, this script itself: a copy that is # not the signed install-native.ps1 (an old or changed one) stops. # 2. Downloads the release's SHA256SUMS and SHA256SUMS.asc and checks the # signature with the Zecnero release key carried in this script # (fingerprint FFFC 8AC0 2BF7 B4B4 50B4 3B1F 6532 09A3 44E7 5767). Windows # has no gpg, so the check is done here: an OpenPGP v4 Ed25519 signature, # checked with the RFC 8032 arithmetic below and .NET's SHA-2. Nothing is # ever installed on checksums alone, from either host. Then it downloads # RELEASE, which the signed SHA256SUMS lists, and stops unless it matches # and holds exactly the name of the release LATEST named: LATEST is not # signed, so this is what stops an older release served under a newer # name. A release without RELEASE (before testnet-v0.2.0) is refused. # 3. Downloads xmrig-zecnero-windows-x86_64.zip, zecnero-wallet-windows-x86_64.zip # and zecnero-miner.ps1, checks each against the signed SHA256SUMS, unpacks # the zips (they must hold exactly xmrig-zecnero.exe and zecnero-wallet.exe: # no driver, no WinRing0) and checks that each program starts. # 4. Installs them into %LOCALAPPDATA%\Zecnero\miner\bin and hands over to that # zecnero-miner.ps1 for the setup: the pool login (a new wallet made with # zecnero-wallet.exe, whose seed it shows once with a SAVED step, or a # Sapling address you have), xmrig's config.json (huge pages when Windows # allows them, no 1 GB pages, no MSR changes), two optional steps, each # asked: Start menu and desktop shortcuts, and starting with Windows when # you sign in (default no; a shortcut in your own Startup folder). For huge # pages it says whether this account has the "Lock pages in memory" right # and, if not, how to give it yourself (zecnero-miner huge-pages on). # It never asks for administrator rights, never changes Windows security # settings, user rights or Microsoft Defender's settings. If Defender blocks the # miner, it says why and how to check the download, and stops. # # Commands (in %LOCALAPPDATA%\Zecnero\miner, or from the Start menu shortcuts): # zecnero-miner start [--minimized] start mining (xmrig in its own window) # zecnero-miner stop stop mining # zecnero-miner status hashrate, shares, pool, huge pages # zecnero-miner update [--force] install the latest release, checked as above; # --force: also one older than installed # zecnero-miner pool-login [new|ntestsapling1...] show or change the pool login # zecnero-miner register-payout ntestsapling1... [--key-file FILE] # have the pool pay what it owes an nm... # mining address to a Sapling address # zecnero-miner huge-pages [on|off] whether this account has the "Lock pages in # memory" right; on/off: the steps to give or # remove it yourself (it changes nothing) # zecnero-miner autostart [on|off] start mining when you sign in to Windows # zecnero-miner shortcuts [on|off] Start menu (and desktop) shortcuts # zecnero-miner config write xmrig's config.json again # zecnero-miner uninstall remove the programs, keep your seed and wallet # Any command takes --pause: wait for Enter before the window closes, or # --pause-on-error: only when it stopped with an error (the shortcuts use these). # # Settings (all optional): # $env:ZECNERO_POOL_LOGIN = "ntestsapling1..." or "new" answer the login question # $env:ZECNERO_WORKER = "name" the worker name (default: this computer's name) # $env:ZECNERO_MIRROR_FIRST = "1" try mirror.zecnero.org first # $env:ZECNERO_SHORTCUTS, $env:ZECNERO_AUTOSTART, # $env:ZECNERO_START = "yes" or "no" answer those questions # $env:HTTPS_PROXY, $env:ALL_PROXY, $env:NO_PROXY a proxy for the downloads: # http://[user:password@]host:port. Without # them, the Windows proxy setting (Settings > # Network > Proxy) is used. PowerShell cannot # use a SOCKS proxy; one is left out, saying so. # Settings for testing: # $env:ZECNERO_NATIVE_HOME = "C:\..." install here instead of %LOCALAPPDATA%\Zecnero\miner # $env:ZECNERO_RELEASE_BASE, $env:ZECNERO_MIRROR_BASE = "https://..." download from # these (http://127.0.0.1:PORT and *.zecnero.test # only with $env:ZECNERO_TEST_MODE = "1") # $env:ZECNERO_POOL_WEB = "https://..." or "http://127.0.0.1:PORT" another pool API # $env:ZECNERO_TEST_KEY_FILE = "C:\...\key.asc", $env:ZECNERO_TEST_FINGERPRINT = "40 hex" # trust this key instead (with ZECNERO_TEST_MODE=1) # $env:ZECNERO_TEST_TIMEOUT = "3" the quick timeout, in seconds (test mode) # $env:ZECNERO_TEST_DESKTOP = "C:\..." the desktop folder (test mode; none without it) # $env:ZECNERO_WALLET_PASSPHRASE, $env:ZECNERO_ASSUME_SAVED = "1", # $env:ZECNERO_REGISTER_CONFIRM = "REGISTER" answer those prompts # $env:ZECNERO_DRY_RUN = "1" say what would change outside the install # folder (shortcuts, autostart) instead of # changing it # $env:ZECNERO_PS_NO_MAIN = "1" load the functions only # $env:ZECNERO_PS_FAKE_LOCK_PAGES = "active" or "missing" # $env:ZECNERO_PS_FAKE_SECPOL = "yes" or "no" whether Windows has secpol.msc $ErrorActionPreference = 'Stop' $Script:Argv = @($args) $Script:RunAsFile = -not [string]::IsNullOrEmpty($PSCommandPath) $Script:SelfPath = $PSCommandPath $Script:InstallCommand = 'irm https://downloads.zecnero.org/install-native.ps1 | iex' $Script:WslCommand = 'irm https://downloads.zecnero.org/install.ps1 | iex' $Script:PrimaryBase = 'https://downloads.zecnero.org' $Script:MirrorBase = 'https://mirror.zecnero.org' $Script:PoolHost = 'stratum.zecnero.org' $Script:PoolTlsPort = 3334 $Script:PoolPlainPort = 3333 $Script:PoolWebDefault = 'https://pool.zecnero.org' # The fallback algorithm for a job that names none. xmrig-zecnero takes each # job's algorithm from the job, so the pool switches it to rx/zecnero2 at NU2. $Script:DefaultAlgo = 'rx/zecnero' $Script:KnownAlgos = @('rx/zecnero', 'rx/zecnero2') $Script:DefaultApiPort = 18088 $Script:SaplingHrp = 'ntestsapling' $Script:LogMaxBytes = 10MB # The release files for native Windows, and what each zip must hold: exactly # these names, nothing else (no WinRing0x64.sys: this miner loads no driver). $Script:ReleaseFiles = [ordered]@{ 'xmrig-zecnero-windows-x86_64.zip' = @('xmrig-zecnero.exe') 'zecnero-wallet-windows-x86_64.zip' = @('zecnero-wallet.exe') 'zecnero-miner.ps1' = $null } $Script:Programs = @('xmrig-zecnero.exe', 'zecnero-wallet.exe', 'zecnero-miner.ps1') # The Zecnero release signing key, FFFC 8AC0 2BF7 B4B4 50B4 3B1F 6532 09A3 44E7 5767, # the same key block install-miner.sh and install.ps1 carry. Its fingerprint is # computed from the key packet and compared with this one before use. $Script:ReleaseFingerprint = 'FFFC8AC02BF7B4B450B43B1F653209A344E75767' $Script:ReleaseKey = @' -----BEGIN PGP PUBLIC KEY BLOCK----- mDMEaq67ARYJKwYBBAHaRw8BAQdAPPAYHc+Eep6d+p1zS3LypG+BWxgBQGMRkea4 DpwzPj+0G3plY25lcm8gPHplY25lcm9AcHJvdG9uLm1lPoivBBMWCgBXFiEE//yK wCv3tLRQtDsfZTIJo0TnV2cFAmquuwEbFIAAAAAABAAObWFudTIsMi41KzEuMTIs MiwxAhsDBQsJCAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEGUyCaNE51dnL0sA /1As3HtllJCcjejp2xd+9DuWLLx9hTSnUSw6ELrycA8KAQDdXzP/+yAxuuWh6QD2 dCa32OkpcDUn6EYPBEbM4MayB7g4BGquuwESCisGAQQBl1UBBQEBB0DEooQVfkdD TibibpLEln7uHuERwthV3xxB/D/39GObTgMBCAeIlAQYFgoAPBYhBP/8isAr97S0 ULQ7H2UyCaNE51dnBQJqrrsBGxSAAAAAAAQADm1hbnUyLDIuNSsxLjEyLDIsMQIb DAAKCRBlMgmjROdXZyOrAPoCI//0w/Ajtqlgy+CvihVwsytgAo0DCi1ySe8nSUGE EgEAzxgHzl7mXOfjp6DeSW7PmjDwayXrpkiGt9aukAnaLg4= =8MCI -----END PGP PUBLIC KEY BLOCK----- '@ # ---------------------------------------------------------------- output and questions function Write-Say([string]$Text) { Write-Host $Text } function Write-Box([string[]]$Lines, [string]$Color = 'Yellow') { $width = ($Lines | Measure-Object -Property Length -Maximum).Maximum $bar = '#' * ($width + 6) Write-Host $bar -ForegroundColor $Color foreach ($line in $Lines) { Write-Host ('# ' + $line.PadRight($width) + ' #') -ForegroundColor $Color } Write-Host $bar -ForegroundColor $Color } # Stops the command: the box is shown by Invoke-Main, which never closes the # PowerShell window (an iex run shares it with the user). function Stop-Setup([string[]]$Lines) { $Script:StopLines = $Lines throw 'ZECNERO_STOP' } function Test-DryRun { return ($env:ZECNERO_DRY_RUN -eq '1') } function Test-TestMode { return ($env:ZECNERO_TEST_MODE -eq '1') } # False in a PowerShell started with -NonInteractive (as the tests do), where # Read-Host would fail: the questions then take their non-interactive default. function Test-Interactive { if ($env:ZECNERO_NONINTERACTIVE -eq '1') { return $false } if (-not [Environment]::UserInteractive) { return $false } foreach ($a in [Environment]::GetCommandLineArgs()) { if ($a -match '^-noni') { return $false } } return $true } function Read-YesNo([string]$Question, [bool]$Default) { if ($Default) { $hint = '[Y/n]' } else { $hint = '[y/N]' } while ($true) { $answer = Read-Host "$Question $hint" if ([string]::IsNullOrWhiteSpace($answer)) { return $Default } switch -Regex ($answer.Trim()) { '^(y|yes)$' { return $true } '^(n|no)$' { return $false } } } } # A yes/no answer from $env: ("yes" or "no"), else asked, else (not # interactive) $Quiet. function Get-Answer([string]$Name, [string]$Question, [bool]$Default, [bool]$Quiet) { $value = "$([Environment]::GetEnvironmentVariable($Name))".Trim() if ($value -match '^(y|yes|on|1|true)$') { return $true } if ($value -match '^(n|no|off|0|false)$') { return $false } if (-not (Test-Interactive)) { return $Quiet } return (Read-YesNo $Question $Default) } function Read-Secret([string]$Prompt) { $secure = Read-Host $Prompt -AsSecureString $ptr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr) } } # ---------------------------------------------------------------- folders and settings function Get-NativeHome { $h = "$env:ZECNERO_NATIVE_HOME".Trim() if ($h -eq '') { $h = Join-Path $env:LOCALAPPDATA 'Zecnero\miner' } return $h.TrimEnd('\') } function Get-Paths { $h = Get-NativeHome return [pscustomobject]@{ Home = $h Bin = Join-Path $h 'bin' Previous = Join-Path $h 'bin.previous' Conf = Join-Path $h 'config' Logs = Join-Path $h 'logs' Run = Join-Path $h 'run' Download = Join-Path $h '.download' Settings = Join-Path $h 'config\miner.json' Xmrig = Join-Path $h 'config\xmrig.json' Seed = Join-Path $h 'POOL-WALLET-SEED.txt' Key = Join-Path $h 'MINING-KEY.txt' Shim = Join-Path $h 'zecnero-miner.cmd' Pid = Join-Path $h 'run\xmrig.pid' Log = Join-Path $h 'logs\xmrig.log' } } # zecnero-wallet's default Testnet wallet on Windows (its data folder is the # roaming AppData), so that `zecnero-wallet balance` finds it with no options. function Get-WalletFile { return (Join-Path $env:APPDATA 'zecnero-wallet\testnet.wallet') } function Write-Utf8File([string]$Path, [string]$Text) { [IO.File]::WriteAllText($Path, $Text, (New-Object Text.UTF8Encoding $false)) } function Read-Settings { $s = [ordered]@{} $file = (Get-Paths).Settings if (Test-Path -LiteralPath $file) { $json = Get-Content -Raw -LiteralPath $file | ConvertFrom-Json foreach ($p in $json.PSObject.Properties) { $s[$p.Name] = $p.Value } } return $s } function Save-Settings($Settings) { $p = Get-Paths New-Item -ItemType Directory -Force -Path $p.Conf | Out-Null Write-Utf8File $p.Settings (([pscustomobject]$Settings) | ConvertTo-Json) } function Set-Setting([string]$Name, $Value) { $s = Read-Settings $s[$Name] = $Value Save-Settings $s } function New-Token { $bytes = New-Object byte[] 24 [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) return (ConvertTo-Hex $bytes).ToLowerInvariant() } function Get-InstalledVersion { $file = Join-Path (Get-Paths).Bin 'VERSION' if (-not (Test-Path -LiteralPath $file)) { return $null } return (Get-Content -Raw -LiteralPath $file).Trim() } function Test-Installed { $bin = (Get-Paths).Bin foreach ($f in $Script:Programs) { if (-not (Test-Path -LiteralPath (Join-Path $bin $f))) { return $false } } return $true } # ---------------------------------------------------------------- where to download from # The hosts to download from, in order: downloads.zecnero.org, then the mirror; # the mirror first with ZECNERO_MIRROR_FIRST=1; ZECNERO_RELEASE_BASE alone, # unless ZECNERO_MIRROR_BASE names a second. $null for an address that is not # https (http://127.0.0.1 and *.zecnero.test are for the tests, in test mode only). function Get-DownloadBases { $release = "$env:ZECNERO_RELEASE_BASE".Trim().TrimEnd('/') $mirror = "$env:ZECNERO_MIRROR_BASE".Trim().TrimEnd('/') if ($release -ne '') { $bases = @($release) } else { $bases = @($Script:PrimaryBase) } if ($mirror -ne '') { $bases += $mirror } elseif ($release -eq '') { $bases += $Script:MirrorBase } if ($env:ZECNERO_MIRROR_FIRST -eq '1' -and $bases.Count -eq 2) { $bases = @($bases[1], $bases[0]) } foreach ($base in $bases) { if ($base -cmatch '^https://[A-Za-z0-9.-]+(:\d+)?(/[A-Za-z0-9._~/-]*)?$') { continue } if ((Test-TestMode) -and $base -cmatch '^http://(127\.0\.0\.1|[a-z0-9-]+\.zecnero\.test):\d+(/[A-Za-z0-9._~/-]*)?$') { continue } return $null } return , $bases } function Get-HostName([string]$Url) { return ([uri]$Url).Authority } # The key downloads are checked with: the release key, or in test mode a key # file with its fingerprint. Stops when the key is not the one pinned. function Get-TrustKey { $fpr = $Script:ReleaseFingerprint $armored = $Script:ReleaseKey if (Test-TestMode) { $tf = "$env:ZECNERO_TEST_FINGERPRINT".Trim().ToUpperInvariant() $file = "$env:ZECNERO_TEST_KEY_FILE".Trim() if ($tf -ne '' -or $file -ne '') { if ($tf -notmatch '^[0-9A-F]{40}$' -or -not (Test-Path -LiteralPath $file)) { Stop-Setup @('ZECNERO_TEST_FINGERPRINT or ZECNERO_TEST_KEY_FILE cannot be used.', 'In test mode they need a 40-character fingerprint and a key file, both set.') } $fpr = $tf $armored = Get-Content -Raw -LiteralPath $file } } elseif ("$env:ZECNERO_TEST_FINGERPRINT$env:ZECNERO_TEST_KEY_FILE" -ne '') { Write-Say 'Ignoring ZECNERO_TEST_FINGERPRINT and ZECNERO_TEST_KEY_FILE, which only work with ZECNERO_TEST_MODE=1.' } $key = $null try { $key = Get-PgpKey $armored } catch { $key = $null } if ($null -eq $key -or $key.Fpr -ne $fpr) { $got = 'nothing' if ($null -ne $key) { $got = $key.Fpr } Stop-Setup @('the signing key is not the Zecnero release key.', "Expected fingerprint $fpr, got $got.", 'Nothing was installed.') } return $key } # Sets up one run's downloads: the hosts, the key, and the test mode notice. function Initialize-Trust { $bases = Get-DownloadBases if ($null -eq $bases) { Stop-Setup @('ZECNERO_RELEASE_BASE or ZECNERO_MIRROR_BASE is not an https address.', "ZECNERO_RELEASE_BASE is `"$env:ZECNERO_RELEASE_BASE`", ZECNERO_MIRROR_BASE is `"$env:ZECNERO_MIRROR_BASE`".", 'Remove them to use downloads.zecnero.org and its mirror:', 'Remove-Item Env:ZECNERO_RELEASE_BASE, Env:ZECNERO_MIRROR_BASE') } $Script:Bases = $bases $Script:BaseIndex = 0 $Script:Trust = Get-TrustKey if (Test-TestMode) { Write-Box @('TEST MODE: NOT FOR REAL USE', 'ZECNERO_TEST_MODE=1 is set. This run trusts the signing key', "fingerprint $($Script:Trust.Fpr)", "and downloads from $($bases -join ' or ')", 'If you did not set this on purpose, stop now (Ctrl+C) and run:', 'Remove-Item Env:ZECNERO_TEST_MODE') 'Yellow' } try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { } foreach ($note in (Get-ProxyNotes)) { Write-Say $note } } function Get-Base { return $Script:Bases[$Script:BaseIndex] } function Test-HasFallback { return ($Script:BaseIndex + 1 -lt $Script:Bases.Count) } function Switch-Host([string]$Why) { Write-Say "$(Get-HostName (Get-Base)) did not work from here ($Why)." $Script:BaseIndex++ Write-Say "Downloading everything from $(Get-HostName (Get-Base)) instead; it is checked the same way." } # ---------------------------------------------------------------- proxies function Hide-Password([string]$Proxy) { return ($Proxy -replace '^([a-z0-9]+://)?[^/@]*@', '$1***@') } function Test-NoProxy([string]$HostName) { foreach ($entry in ("$([Environment]::GetEnvironmentVariable('NO_PROXY'))" -split '[,\s]+')) { $e = $entry.Trim().TrimStart('*').TrimStart('.').ToLowerInvariant() if ($e -eq '') { if ($entry.Trim() -eq '*') { return $true }; continue } $h = $HostName.ToLowerInvariant() if ($h -eq $e -or $h.EndsWith(".$e")) { return $true } } return $false } # The proxy for a URL from HTTPS_PROXY (HTTP_PROXY for http://) or ALL_PROXY: # @{ Uri; Credential; Name; Shown }, or $null for none (the Windows setting # applies then). Windows variable names ignore case. function Get-EnvProxy([string]$Url) { $u = [uri]$Url if (Test-NoProxy $u.Host) { return $null } $names = @('HTTPS_PROXY', 'ALL_PROXY') if ($u.Scheme -eq 'http') { $names = @('HTTP_PROXY', 'ALL_PROXY') } foreach ($name in $names) { $value = "$([Environment]::GetEnvironmentVariable($name))".Trim() if ($value -eq '') { continue } if ($value -notmatch '^http://') { continue } if ($value -notmatch '^http://(([^:@/]+)(:([^@/]*))?@)?([A-Za-z0-9.-]+|\[[0-9a-fA-F:]+\])(:(\d+))?/?$') { continue } $cred = $null if ($Matches[2]) { $pw = New-Object Security.SecureString foreach ($c in ([uri]::UnescapeDataString("$($Matches[4])")).ToCharArray()) { $pw.AppendChar($c) } $cred = New-Object Management.Automation.PSCredential ([uri]::UnescapeDataString($Matches[2])), $pw } $port = $Matches[7] if (-not $port) { $port = '80' } return [pscustomobject]@{ Uri = "http://$($Matches[5]):$port"; Credential = $cred; Name = $name; Shown = (Hide-Password $value) } } return $null } # What to say about proxies before the downloads. function Get-ProxyNotes { $notes = @() foreach ($name in @('HTTPS_PROXY', 'ALL_PROXY')) { $value = "$([Environment]::GetEnvironmentVariable($name))".Trim() if ($value -eq '') { continue } if ($value -match '^socks') { $notes += "Not using $name ($(Hide-Password $value)): PowerShell cannot use a SOCKS proxy. Set an http:// proxy in HTTPS_PROXY, or use the Windows proxy setting (Settings > Network > Proxy)." } elseif ($value -notmatch '^http://') { $notes += "Not using $name ($(Hide-Password $value)): only http:// proxies work here." } } $proxy = Get-EnvProxy "$(Get-Base)/LATEST" if ($proxy) { $notes += "Downloading through the proxy in $($proxy.Name): $($proxy.Shown)" } return $notes } function Get-WebParams([string]$Url, [int]$Timeout) { $params = @{ Uri = $Url; UseBasicParsing = $true; TimeoutSec = $Timeout; ErrorAction = 'Stop' } $proxy = Get-EnvProxy $Url if ($proxy) { $params.Proxy = $proxy.Uri if ($proxy.Credential) { $params.ProxyCredential = $proxy.Credential } } return $params } # Why a web request failed, in words. function Get-WebWhy($ErrorRecord) { $ex = $ErrorRecord.Exception while ($null -ne $ex -and -not ($ex -is [Net.WebException]) -and $null -ne $ex.InnerException) { $ex = $ex.InnerException } if ($ex -is [Net.WebException]) { switch ([string]$ex.Status) { 'NameResolutionFailure' { return 'the name could not be resolved' } 'ProxyNameResolutionFailure' { return 'the proxy name could not be resolved' } 'ConnectFailure' { return 'no connection' } 'Timeout' { return 'timed out' } 'TrustFailure' { return 'its TLS certificate was refused' } 'SecureChannelFailure' { return 'the TLS handshake failed' } 'ProtocolError' { if ($ex.Response) { return "HTTP $([int]$ex.Response.StatusCode)" } return 'an HTTP error' } default { return "the connection broke off ($($ex.Status))" } } } $message = "$($ErrorRecord.Exception.Message)" -split "`r?`n" | Select-Object -First 1 return $message } # Invoke-Fetch URL FILE [-Small]: $null when it worked, else why not. While # there is another host to turn to, a host gets 10 s for a small file. function Invoke-Fetch([string]$Url, [string]$OutFile, [switch]$Small) { $quick = 10 if ((Test-TestMode) -and "$env:ZECNERO_TEST_TIMEOUT" -match '^\d+$') { $quick = [int]$env:ZECNERO_TEST_TIMEOUT } if (Test-HasFallback) { if ($Small) { $timeout = $quick } else { $timeout = 300 } } else { if ($Small) { $timeout = 60 } else { $timeout = 900 } } $params = Get-WebParams $Url $timeout $params.OutFile = $OutFile $previous = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { Invoke-WebRequest @params | Out-Null return $null } catch { Remove-Item -LiteralPath $OutFile -Force -ErrorAction SilentlyContinue return (Get-WebWhy $_) } finally { $ProgressPreference = $previous } } # The current release folder name, as LATEST names it, checked: LATEST, # INSTALLERS.SHA256SUMS and its signature come from one host (the next one # when a host does not answer, never when a check fails); the signature must be # good (Test-SignedFile), and LATEST must match its one entry there. The signed # list stays in $Script:InstallerSums for Test-InstallerItself. function Get-LatestVersion { $p = Get-Paths New-Item -ItemType Directory -Force -Path $p.Download | Out-Null $work = Join-Path $p.Download "latest.$([IO.Path]::GetRandomFileName())" New-Item -ItemType Directory -Force -Path $work | Out-Null $Script:CleanupDirs += $work $files = @('LATEST', 'INSTALLERS.SHA256SUMS', 'INSTALLERS.SHA256SUMS.asc') while ($true) { $failed = $null foreach ($f in $files) { $why = Invoke-Fetch "$(Get-Base)/$f" (Join-Path $work $f) -Small if ($why) { $failed = @($f, $why); break } } if ($null -eq $failed) { break } if (Test-HasFallback) { Switch-Host $failed[1] Get-ChildItem -LiteralPath $work -Force | Remove-Item -Force continue } if ($failed[0] -eq 'INSTALLERS.SHA256SUMS.asc') { Stop-Setup @("could not download the signature of INSTALLERS.SHA256SUMS from $(Get-HostName (Get-Base)) ($($failed[1])).", 'Files are accepted only with a good signature from the Zecnero', 'release key, never on checksums alone. Nothing was installed.') } Stop-Setup @("could not download $(Get-Base)/$($failed[0]) ($($failed[1])).", 'Check your internet connection and try again. Behind a proxy, set', 'HTTPS_PROXY (http://host:port) before the command, or the Windows', 'proxy setting (Settings > Network > Proxy).') } $hostName = Get-HostName (Get-Base) Test-SignedFile (Join-Path $work 'INSTALLERS.SHA256SUMS') (Join-Path $work 'INSTALLERS.SHA256SUMS.asc') 'INSTALLERS.SHA256SUMS' $hostName $sums = [IO.File]::ReadAllText((Join-Path $work 'INSTALLERS.SHA256SUMS')) $want = Get-SumEntry $sums 'LATEST' if ($null -eq $want) { Stop-Setup @("INSTALLERS.SHA256SUMS from $hostName has no single entry for LATEST.", 'Nothing was installed.') } $got = Get-FileSha256 (Join-Path $work 'LATEST') if ($got -cne $want) { Stop-Setup @("LATEST from $hostName is not the one the signed INSTALLERS.SHA256SUMS lists.", "expected $want", "got $got", 'Nothing was installed. A release may be in the middle of being', 'published: try again in a few minutes. If it stays like this, please', 'report it at https://gitlab.com/zecnero or on Discord.') } $Script:InstallerSums = $sums $bytes = [IO.File]::ReadAllBytes((Join-Path $work 'LATEST')) $count = [Math]::Min($bytes.Length, 64) $version = ([Text.Encoding]::ASCII.GetString($bytes, 0, $count)).Trim() if ($version -notmatch '^testnet-v[0-9]+\.[0-9]+\.[0-9]+$') { Stop-Setup @('the release pointer is not a version name.', "$(Get-Base)/LATEST says: $version") } return $version } function ConvertTo-VersionNumber([string]$Version) { if ($Version -match '^testnet-v([0-9]+)\.([0-9]+)\.([0-9]+)$') { return [version]"$($Matches[1]).$($Matches[2]).$($Matches[3])" } return $null } # ---------------------------------------------------------------- OpenPGP, without gpg # # Only what the release signature needs: a version 4 detached signature of # type 0x00 (binary document), made with the pinned EdDSA (Ed25519) key over # SHA-256, SHA-384 or SHA-512 (RFC 4880 5.2.4, and RFC 8032 for Ed25519, whose # message is the digest). Anything else is refused, never skipped. function ConvertTo-Hex([byte[]]$Bytes) { return ([BitConverter]::ToString($Bytes) -replace '-', '') } function Get-Slice([byte[]]$A, [int]$Offset, [int]$Count) { if ($null -eq $A -or $Count -lt 0 -or $Offset -lt 0 -or $Offset + $Count -gt $A.Length) { return $null } $r = New-Object byte[] $Count if ($Count -gt 0) { [Array]::Copy($A, $Offset, $r, 0, $Count) } return , $r } # The bytes inside ASCII armor, or $null. function ConvertFrom-PgpArmor([string]$Text) { $inside = $false $body = $false $b64 = New-Object Text.StringBuilder foreach ($line in ($Text -split "`r?`n")) { $t = $line.Trim() if (-not $inside) { if ($t -match '^-----BEGIN PGP (SIGNATURE|PUBLIC KEY BLOCK)-----$') { $inside = $true } continue } if ($t -match '^-----END PGP ') { break } if (-not $body) { if ($t -eq '') { $body = $true } continue } if ($t.StartsWith('=')) { continue } [void]$b64.Append($t) } if (-not $body -or $b64.Length -eq 0) { return $null } try { return , [Convert]::FromBase64String($b64.ToString()) } catch { return $null } } function Read-BigEndian([byte[]]$A, [int]$Offset, [int]$Count) { [long]$v = 0 for ($k = 0; $k -lt $Count; $k++) { $v = ($v * 256) + [long]$A[$Offset + $k] } return $v } # The packets in a binary OpenPGP message, as @{ Tag; Body }, or $null. function Read-PgpPackets([byte[]]$Data) { $packets = New-Object Collections.ArrayList $i = 0 $n = $Data.Length while ($i -lt $n) { $h = [int]$Data[$i] $i++ if (($h -band 0x80) -eq 0) { return $null } if (($h -band 0x40) -ne 0) { $tag = $h -band 0x3f if ($i -ge $n) { return $null } $l1 = [int]$Data[$i] $i++ if ($l1 -lt 192) { $len = [long]$l1 } elseif ($l1 -lt 224) { if ($i -ge $n) { return $null } $len = [long](($l1 - 192) * 256 + [int]$Data[$i] + 192) $i++ } elseif ($l1 -eq 255) { if ($i + 4 -gt $n) { return $null } $len = Read-BigEndian $Data $i 4 $i += 4 } else { return $null } } else { $tag = ($h -shr 2) -band 0x0f $lt = $h -band 3 if ($lt -eq 3) { return $null } $size = @(1, 2, 4)[$lt] if ($i + $size -gt $n) { return $null } $len = Read-BigEndian $Data $i $size $i += $size } if ($len -gt ($n - $i)) { return $null } $body = Get-Slice $Data $i ([int]$len) [void]$packets.Add([pscustomobject]@{ Tag = $tag; Body = $body }) $i += [int]$len } return , $packets.ToArray() } function Read-PgpSubpackets([byte[]]$Area) { $list = New-Object Collections.ArrayList $i = 0 $n = $Area.Length while ($i -lt $n) { $l1 = [int]$Area[$i] $i++ if ($l1 -lt 192) { $len = $l1 } elseif ($l1 -lt 255) { if ($i -ge $n) { return $null } $len = ($l1 - 192) * 256 + [int]$Area[$i] + 192 $i++ } else { if ($i + 4 -gt $n) { return $null } $len = [int](Read-BigEndian $Area $i 4) $i += 4 } if ($len -lt 1 -or $len -gt ($n - $i)) { return $null } $type = [int]$Area[$i] [void]$list.Add([pscustomobject]@{ Type = ($type -band 0x7f); Critical = (($type -band 0x80) -ne 0); Data = (Get-Slice $Area ($i + 1) ($len - 1)) }) $i += $len } return , $list.ToArray() } # A version 4 signature packet's fields, or $null. function Read-PgpSignature([byte[]]$B) { if ($B.Length -lt 10 -or $B[0] -ne 4) { return $null } $hl = [int]$B[4] * 256 + [int]$B[5] $hashedEnd = 6 + $hl if ($hashedEnd + 2 -gt $B.Length) { return $null } $ul = [int]$B[$hashedEnd] * 256 + [int]$B[$hashedEnd + 1] $j = $hashedEnd + 2 + $ul if ($j + 2 -gt $B.Length) { return $null } $hashedSub = Read-PgpSubpackets (Get-Slice $B 6 $hl) $unhashedSub = Read-PgpSubpackets (Get-Slice $B ($hashedEnd + 2) $ul) if ($null -eq $hashedSub -or $null -eq $unhashedSub) { return $null } $left = Get-Slice $B $j 2 $j += 2 $mpis = New-Object Collections.ArrayList while ($j -lt $B.Length) { if ($j + 2 -gt $B.Length) { return $null } $bits = [int]$B[$j] * 256 + [int]$B[$j + 1] $j += 2 $len = [int][Math]::Floor(($bits + 7) / 8) $m = Get-Slice $B $j $len if ($null -eq $m) { return $null } [void]$mpis.Add($m) $j += $len } $sig = [pscustomobject]@{ Type = [int]$B[1]; PkAlgo = [int]$B[2]; HashAlgo = [int]$B[3] Hashed = (Get-Slice $B 0 $hashedEnd); Left = $left; Mpis = $mpis.ToArray() IssuerFpr = $null; IssuerKeyId = $null; Created = $null; Expires = $null; CriticalUnknown = $false } foreach ($sp in $hashedSub) { switch ($sp.Type) { 2 { if ($sp.Data.Length -eq 4) { $sig.Created = Read-BigEndian $sp.Data 0 4 } } 3 { if ($sp.Data.Length -eq 4) { $sig.Expires = Read-BigEndian $sp.Data 0 4 } } 16 { } 33 { } default { if ($sp.Critical) { $sig.CriticalUnknown = $true } } } } foreach ($sp in @($hashedSub) + @($unhashedSub)) { if ($sp.Type -eq 33 -and $sp.Data.Length -eq 21 -and $sp.Data[0] -eq 4 -and $null -eq $sig.IssuerFpr) { $sig.IssuerFpr = ConvertTo-Hex (Get-Slice $sp.Data 1 20) } if ($sp.Type -eq 16 -and $sp.Data.Length -eq 8 -and $null -eq $sig.IssuerKeyId) { $sig.IssuerKeyId = ConvertTo-Hex $sp.Data } } return $sig } # The first key packet of an armored public key: @{ Fpr; KeyId; Pub }, for a # version 4 EdDSA key on Ed25519 only, or $null. function Get-PgpKey([string]$Armored) { $bin = ConvertFrom-PgpArmor $Armored if ($null -eq $bin) { return $null } $packets = Read-PgpPackets $bin if ($null -eq $packets -or $packets.Count -eq 0 -or $packets[0].Tag -ne 6) { return $null } $b = $packets[0].Body if ($b.Length -lt 8 -or $b[0] -ne 4 -or $b[5] -ne 22) { return $null } $oidLen = [int]$b[6] if ((ConvertTo-Hex (Get-Slice $b 7 $oidLen)) -ne '2B06010401DA470F01') { return $null } $j = 7 + $oidLen if ($j + 2 + 33 -ne $b.Length) { return $null } $bits = [int]$b[$j] * 256 + [int]$b[$j + 1] if ($bits -ne 263 -or $b[$j + 2] -ne 0x40) { return $null } $pub = Get-Slice $b ($j + 3) 32 $prefix = [byte[]](0x99, [byte](($b.Length -shr 8) -band 0xff), [byte]($b.Length -band 0xff)) $sha1 = [Security.Cryptography.SHA1]::Create() [void]$sha1.TransformBlock($prefix, 0, 3, $null, 0) [void]$sha1.TransformFinalBlock($b, 0, $b.Length) $fpr = ConvertTo-Hex $sha1.Hash return [pscustomobject]@{ Fpr = $fpr; KeyId = $fpr.Substring(24); Pub = $pub } } # Ed25519 (RFC 8032) with System.Numerics.BigInteger: points in extended # coordinates (X, Y, Z, T). Verification only, so nothing here is secret. $Script:Ed = $null function Get-EdParams { if ($null -ne $Script:Ed) { return $Script:Ed } $p = [bigint]::Pow(2, 255) - 19 $L = [bigint]::Pow(2, 252) + [bigint]::Parse('27742317777372353535851937790883648493') $d = ((-121665) * [bigint]::ModPow(121666, $p - 2, $p)) % $p if ($d.Sign -lt 0) { $d += $p } $Script:Ed = @{ P = $p; L = $L; D = $d; D2 = ((2 * $d) % $p); SqrtM1 = [bigint]::ModPow(2, ($p - 1) / 4, $p) } $gy = (4 * [bigint]::ModPow(5, $p - 2, $p)) % $p $gx = Get-EdRecoverX $gy 0 $Script:Ed.B = @($gx, $gy, [bigint]::One, (($gx * $gy) % $p)) return $Script:Ed } function Get-EdRecoverX([bigint]$y, [int]$sign) { $e = $Script:Ed $p = $e.P if ($y -ge $p) { return $null } $x2 = (($y * $y - 1) * [bigint]::ModPow(($e.D * $y * $y + 1) % $p, $p - 2, $p)) % $p if ($x2.Sign -lt 0) { $x2 += $p } if ($x2.IsZero) { if ($sign -ne 0) { return $null } return [bigint]::Zero } $x = [bigint]::ModPow($x2, ($p + 3) / 8, $p) if ((($x * $x - $x2) % $p) -ne 0) { $x = ($x * $e.SqrtM1) % $p } if ((($x * $x - $x2) % $p) -ne 0) { return $null } if ([int]($x % 2) -ne $sign) { $x = $p - $x } return $x } function ConvertFrom-LittleEndian([byte[]]$Bytes) { $b = New-Object byte[] ($Bytes.Length + 1) [Array]::Copy($Bytes, $b, $Bytes.Length) return (New-Object Numerics.BigInteger (, $b)) } function Add-EdPoint($P1, $P2) { $e = $Script:Ed $p = $e.P $a = (($P1[1] - $P1[0]) * ($P2[1] - $P2[0])) % $p $b = (($P1[1] + $P1[0]) * ($P2[1] + $P2[0])) % $p $c = ($P1[3] * $e.D2 * $P2[3]) % $p $dd = (2 * $P1[2] * $P2[2]) % $p $ee = $b - $a $f = $dd - $c $g = $dd + $c $h = $b + $a return @((($ee * $f) % $p), (($g * $h) % $p), (($f * $g) % $p), (($ee * $h) % $p)) } function Get-EdMultiple([bigint]$S, $Point) { $q = @([bigint]::Zero, [bigint]::One, [bigint]::One, [bigint]::Zero) while ($S.Sign -gt 0) { if (-not $S.IsEven) { $q = Add-EdPoint $q $Point } $Point = Add-EdPoint $Point $Point $S = $S -shr 1 } return $q } function Test-EdPointEqual($P1, $P2) { $p = $Script:Ed.P if ((($P1[0] * $P2[2] - $P2[0] * $P1[2]) % $p) -ne 0) { return $false } return ((($P1[1] * $P2[2] - $P2[1] * $P1[2]) % $p) -eq 0) } function Get-EdPoint([byte[]]$Encoded) { if ($Encoded.Length -ne 32) { return $null } $b = [byte[]]$Encoded.Clone() $sign = ($b[31] -shr 7) -band 1 $b[31] = $b[31] -band 0x7f $y = ConvertFrom-LittleEndian $b $x = Get-EdRecoverX $y $sign if ($null -eq $x) { return $null } return @($x, $y, [bigint]::One, (($x * $y) % $Script:Ed.P)) } # True when Sig (64 bytes) is a valid Ed25519 signature of Msg by Pub (32 bytes). function Test-Ed25519([byte[]]$Pub, [byte[]]$Msg, [byte[]]$Sig) { $e = Get-EdParams if ($Sig.Length -ne 64 -or $Pub.Length -ne 32) { return $false } $A = Get-EdPoint $Pub if ($null -eq $A) { return $false } $rb = Get-Slice $Sig 0 32 $R = Get-EdPoint $rb if ($null -eq $R) { return $false } $s = ConvertFrom-LittleEndian (Get-Slice $Sig 32 32) if ($s -ge $e.L) { return $false } $sha = [Security.Cryptography.SHA512]::Create() [void]$sha.TransformBlock($rb, 0, 32, $null, 0) [void]$sha.TransformBlock($Pub, 0, 32, $null, 0) [void]$sha.TransformFinalBlock($Msg, 0, $Msg.Length) $k = (ConvertFrom-LittleEndian $sha.Hash) % $e.L $sB = Get-EdMultiple $s $e.B $kA = Get-EdMultiple $k $A return (Test-EdPointEqual $sB (Add-EdPoint $R $kA)) } function Test-PgpSignatureMath([byte[]]$Data, $Sig, $Key) { if ($Sig.Type -ne 0 -or $Sig.PkAlgo -ne 22 -or $Sig.CriticalUnknown) { return $false } if ($null -ne $Sig.Expires -and $Sig.Expires -gt 0 -and $null -ne $Sig.Created) { $now = [long]([DateTime]::UtcNow - [DateTime]'1970-01-01').TotalSeconds if ($now -gt $Sig.Created + $Sig.Expires) { return $false } } switch ($Sig.HashAlgo) { 8 { $h = [Security.Cryptography.SHA256]::Create() } 9 { $h = [Security.Cryptography.SHA384]::Create() } 10 { $h = [Security.Cryptography.SHA512]::Create() } default { return $false } } $n = $Sig.Hashed.Length $trailer = [byte[]](4, 0xff, [byte](($n -shr 24) -band 0xff), [byte](($n -shr 16) -band 0xff), [byte](($n -shr 8) -band 0xff), [byte]($n -band 0xff)) [void]$h.TransformBlock($Data, 0, $Data.Length, $null, 0) [void]$h.TransformBlock($Sig.Hashed, 0, $n, $null, 0) [void]$h.TransformFinalBlock($trailer, 0, $trailer.Length) $digest = $h.Hash if ($null -eq $Sig.Left -or $digest[0] -ne $Sig.Left[0] -or $digest[1] -ne $Sig.Left[1]) { return $false } if ($Sig.Mpis.Count -ne 2) { return $false } $r = [byte[]]$Sig.Mpis[0] $sv = [byte[]]$Sig.Mpis[1] if ($r.Length -gt 32 -or $sv.Length -gt 32) { return $false } $raw = New-Object byte[] 64 [Array]::Copy($r, 0, $raw, 32 - $r.Length, $r.Length) [Array]::Copy($sv, 0, $raw, 64 - $sv.Length, $sv.Length) return (Test-Ed25519 $Key.Pub $digest $raw) } # Checks a detached armored signature of Data: 'good' (a signature by the key # verifies, and none by it fails), 'bad' (one by the key fails), 'otherkey' # (signatures by other keys only) or 'unreadable'. function Test-PgpSignature([byte[]]$Data, [string]$Armored, $Key) { try { $bin = ConvertFrom-PgpArmor $Armored if ($null -eq $bin -or $bin.Length -eq 0) { return 'unreadable' } $packets = Read-PgpPackets $bin if ($null -eq $packets) { return 'unreadable' } $sigs = @($packets | Where-Object { $_.Tag -eq 2 }) if ($sigs.Count -eq 0 -or $sigs.Count -ne $packets.Count) { return 'unreadable' } $good = $false foreach ($packet in $sigs) { $s = Read-PgpSignature $packet.Body if ($null -eq $s) { return 'unreadable' } $issuer = $true if ($null -ne $s.IssuerFpr) { $issuer = ($s.IssuerFpr -eq $Key.Fpr) } elseif ($null -ne $s.IssuerKeyId) { $issuer = ($s.IssuerKeyId -eq $Key.KeyId) } if (-not $issuer) { continue } if (-not (Test-PgpSignatureMath $Data $s $Key)) { return 'bad' } $good = $true } if ($good) { return 'good' } return 'otherkey' } catch { return 'unreadable' } } # ---------------------------------------------------------------- the release, checked function Get-FileSha256([string]$Path) { $stream = [IO.File]::OpenRead($Path) try { return (ConvertTo-Hex ([Security.Cryptography.SHA256]::Create().ComputeHash($stream))).ToLowerInvariant() } finally { $stream.Dispose() } } # The one SHA256SUMS line for a file name, as its hash, or $null. function Get-SumEntry([string]$Sums, [string]$Name) { $hits = @(($Sums -split "`r?`n") | Where-Object { $_ -cmatch ('^[0-9a-f]{64} [ *]' + [regex]::Escape($Name) + '$') }) if ($hits.Count -ne 1) { return $null } return $hits[0].Substring(0, 64) } function Show-DefenderStop([string]$Program, [string]$Detail) { $p = Get-Paths Stop-Setup @("Windows (Microsoft Defender, or another antivirus) blocked $Program.", $Detail, '', 'The download itself was checked before this: SHA256SUMS has a good', 'signature from the Zecnero release key, and every file matches it.', 'Defender flags XMRig miners by pattern, xmrig-zecnero included, even', 'unchanged ones. How to check the download yourself, and what an', 'exclusion means: https://zecnero.org/mine#windows-defender', '', 'If you choose to mine anyway, add an exclusion for this folder only:', " $($p.Home)", '(Windows Security > Virus & threat protection > Manage settings >', 'Exclusions > Add an exclusion > Folder), then run the same command again.', 'This installer never changes Defender settings itself.') } # Runs a program and returns @{ Code; Out; Err }. Env adds variables for the # child only (never this PowerShell's own environment); Stdin is written as # UTF-8 bytes exactly, with no newline added (PowerShell's pipe adds one). function Invoke-Program([string]$File, [string[]]$Arguments, [hashtable]$Env = @{}, [string]$Stdin = $null) { $psi = New-Object Diagnostics.ProcessStartInfo $psi.FileName = $File $psi.Arguments = ($Arguments | ForEach-Object { if ($_ -match '[\s"]') { '"' + $_ + '"' } else { $_ } }) -join ' ' $psi.UseShellExecute = $false $psi.CreateNoWindow = $true $psi.RedirectStandardOutput = $true $psi.RedirectStandardError = $true $psi.RedirectStandardInput = $true foreach ($name in @('ZWALLET_PASSPHRASE', 'ZWALLET_WIF', 'ZWALLET_SEED')) { if ($psi.EnvironmentVariables.ContainsKey($name)) { $psi.EnvironmentVariables.Remove($name) } } foreach ($k in $Env.Keys) { $psi.EnvironmentVariables[$k] = $Env[$k] } $proc = [Diagnostics.Process]::Start($psi) if ($null -ne $Stdin -and $Stdin -ne '') { $bytes = (New-Object Text.UTF8Encoding $false).GetBytes($Stdin) $proc.StandardInput.BaseStream.Write($bytes, 0, $bytes.Length) } $proc.StandardInput.Close() $errTask = $proc.StandardError.ReadToEndAsync() $out = $proc.StandardOutput.ReadToEnd() $proc.WaitForExit() return [pscustomobject]@{ Code = $proc.ExitCode; Out = $out; Err = $errTask.Result } } # Checks that a program starts. Antivirus shows up here as a missing file, a # refusal to open it, or Windows error 225 (the file contains a virus). function Test-ProgramStarts([string]$Path, [string[]]$Arguments) { $name = Split-Path -Leaf $Path Start-Sleep -Milliseconds 300 if (-not (Test-Path -LiteralPath $Path)) { Show-DefenderStop $name "$name was removed right after it was unpacked." } try { $r = Invoke-Program $Path $Arguments } catch { $msg = "$($_.Exception.Message)" $inner = $_.Exception.InnerException if (($inner -is [ComponentModel.Win32Exception] -and ($inner.NativeErrorCode -eq 225 -or $inner.NativeErrorCode -eq 2 -or $inner.NativeErrorCode -eq 5)) -or $msg -match 'virus|unwanted|cannot find the file|Access is denied') { Show-DefenderStop $name "It could not be started: $msg" } Stop-Setup @("$name does not run on this computer.", $msg) } if ($r.Code -ne 0) { Stop-Setup @("$name does not run on this computer (exit code $($r.Code)).", "$($r.Out)$($r.Err)".Trim()) } return (("$($r.Out)" -split "`r?`n") | Select-Object -First 1) } function Expand-ReleaseZip([string]$Zip, [string[]]$Expected, [string]$Dest) { Add-Type -AssemblyName System.IO.Compression.FileSystem $name = Split-Path -Leaf $Zip try { $archive = [IO.Compression.ZipFile]::OpenRead($Zip) } catch { Stop-Setup @("could not open $name.", "$($_.Exception.Message)") } try { $entries = @($archive.Entries | ForEach-Object { $_.FullName }) $bad = @($entries | Where-Object { $_ -notin $Expected }) if ($bad.Count -gt 0 -or $entries.Count -ne $Expected.Count) { $lines = @("$name does not hold what a Zecnero release holds.", "Expected: $($Expected -join ', ')", "Found: $($entries -join ', ')") if (@($entries | Where-Object { $_ -match '\.sys$|WinRing' }).Count -gt 0) { $lines += 'It carries a driver. Zecnero''s Windows miner loads no driver (no WinRing0).' } Stop-Setup ($lines + 'Nothing was installed.') } foreach ($entry in $archive.Entries) { try { [IO.Compression.ZipFileExtensions]::ExtractToFile($entry, (Join-Path $Dest $entry.FullName), $true) } catch { $msg = "$($_.Exception.Message)" if ($msg -match 'virus|unwanted|Access to the path') { Show-DefenderStop $entry.FullName "Unpacking it failed: $msg" } Stop-Setup @("could not unpack $($entry.FullName) from $name.", $msg) } } } finally { $archive.Dispose() } } # The lines to stop with when the signed SHA256SUMS of Version lists no RELEASE. function Get-NoReleaseLines([string]$Version, [string]$HostName) { return @("$Version from $HostName does not say which version it is.", 'Its signed SHA256SUMS lists no RELEASE file. Every release from', 'testnet-v0.2.0 on names its version there, so that an older release', 'cannot be passed off as a newer one. A release without it is never', 'installed. Nothing was installed. If the download server names such', 'a release as the latest, please report it at https://gitlab.com/zecnero', 'or on Discord.') } # Checks the downloaded RELEASE against its entry in the signed SHA256SUMS and # that it holds exactly Version and a newline. Returns Version; stops otherwise. function Test-ReleaseName([string]$Path, [string]$Version, [string]$Want, [string]$HostName) { $got = Get-FileSha256 $Path if ($got -cne $Want) { Stop-Setup @('RELEASE is damaged or was changed (checksum mismatch).', "expected $Want", "got $got", 'Nothing was installed. Try again later.') } $bytes = [IO.File]::ReadAllBytes($Path) $text = [Text.Encoding]::ASCII.GetString($bytes) if ($bytes.Length -ne $Version.Length + 1 -or $text -cne "$Version`n") { $named = ($text.Substring(0, [Math]::Min($text.Length, 64)) -replace '[^A-Za-z0-9.-]', '') if ($named -eq '') { $named = 'not named' } Stop-Setup @("the release in $Version/ on $HostName is $named, not $Version.", 'Its signed SHA256SUMS names the release it belongs to, and it is not', 'the one the folder and LATEST name: an older release served under a', 'newer name. Nothing was installed. Do not run these files.', 'Please report this at https://gitlab.com/zecnero or on Discord.') } return $Version } # Downloads SHA256SUMS and its signature, checks the signature, then RELEASE # (the release the signed SHA256SUMS says this is; see Test-ReleaseName), then # the release files, all from one host (the next one when a host does not # answer, starting again from SHA256SUMS), checks each file's checksum, unpacks # the zips and checks that each program starts. Returns the folder that becomes # bin, whose VERSION is the release RELEASE names. function Get-VerifiedRelease([string]$Version) { $p = Get-Paths $work = Join-Path $p.Download ([IO.Path]::GetRandomFileName()) New-Item -ItemType Directory -Force -Path $work | Out-Null $Script:CleanupDirs += $work $names = @($Script:ReleaseFiles.Keys) while ($true) { $failed = $null $base = "$(Get-Base)/$Version" $host_ = Get-HostName $base foreach ($f in @('SHA256SUMS', 'SHA256SUMS.asc')) { $why = Invoke-Fetch "$base/$f" (Join-Path $work $f) -Small if ($why) { $failed = @($f, $why); break } } if ($null -eq $failed) { Test-ReleaseSignature $work $host_ $sums = [IO.File]::ReadAllText((Join-Path $work 'SHA256SUMS')) $releaseSum = Get-SumEntry $sums 'RELEASE' if ($null -eq $releaseSum) { Stop-Setup (Get-NoReleaseLines $Version $host_) } $why = Invoke-Fetch "$base/RELEASE" (Join-Path $work 'RELEASE') -Small if ($why) { $failed = @('RELEASE', $why) } } if ($null -eq $failed) { $signed = Test-ReleaseName (Join-Path $work 'RELEASE') $Version $releaseSum $host_ $missing = @($names | Where-Object { $null -eq (Get-SumEntry $sums $_) }) if ($missing.Count -gt 0) { Stop-Setup @("$Version has no native Windows miner yet (SHA256SUMS lists no $($missing -join ', ')).", 'Mine through WSL until a release has it. In PowerShell:', " $Script:WslCommand") } foreach ($f in $names) { Write-Say " downloading $f" $why = Invoke-Fetch "$base/$f" (Join-Path $work $f) if ($why) { $failed = @($f, $why); break } } } if ($null -eq $failed) { break } if ($failed[0] -eq 'SHA256SUMS.asc' -and -not (Test-HasFallback)) { Stop-Setup @("could not download the signature of $Version from $host_ ($($failed[1])).", 'Files are accepted only with a good signature from the Zecnero', 'release key, never on checksums alone. Nothing was installed.') } if (Test-HasFallback) { Switch-Host $failed[1] Get-ChildItem -LiteralPath $work -Force | Remove-Item -Recurse -Force continue } if ($Script:BaseIndex -gt 0) { Stop-Setup @("could not download $($failed[0]) ($($failed[1])).", "from $base/$($failed[0])", 'A release published in the last few minutes may not be on', "$host_ yet: the mirror copies it within about 10 minutes. Try again later.") } Stop-Setup @("could not download $($failed[0]) ($($failed[1])).", "from $base/$($failed[0])", 'Check your internet connection and try again.') } Write-Say " signature OK (Zecnero release key $($Script:Trust.Fpr)); the signed release is $signed" foreach ($f in $names) { $path = Join-Path $work $f try { $got = Get-FileSha256 $path } catch { Show-DefenderStop $f "It could not be read after the download: $($_.Exception.Message)" } $want = Get-SumEntry $sums $f if ($got -ne $want) { Stop-Setup @("$f is damaged or was changed (checksum mismatch).", "expected $want", "got $got", 'Nothing was installed. Try again later.') } } Write-Say ' checksums OK' $stage = Join-Path $work 'stage' New-Item -ItemType Directory -Force -Path $stage | Out-Null foreach ($f in $names) { $expected = $Script:ReleaseFiles[$f] if ($null -eq $expected) { Copy-Item -LiteralPath (Join-Path $work $f) -Destination (Join-Path $stage $f) } else { Expand-ReleaseZip (Join-Path $work $f) $expected $stage } } $wallet = Test-ProgramStarts (Join-Path $stage 'zecnero-wallet.exe') @('--version') [void](Test-ProgramStarts (Join-Path $stage 'zecnero-wallet.exe') @('sign-message', '--help')) $miner = Test-ProgramStarts (Join-Path $stage 'xmrig-zecnero.exe') @('--version') Write-Say " $miner; $wallet" Copy-Item -LiteralPath (Join-Path $work 'SHA256SUMS') -Destination (Join-Path $stage 'SHA256SUMS') Copy-Item -LiteralPath (Join-Path $work 'RELEASE') -Destination (Join-Path $stage 'RELEASE') # The installed version is the signed one, never LATEST's word alone. Write-Utf8File (Join-Path $stage 'VERSION') "$signed`r`n" return $stage } # Returns only when Test-PgpSignature answers exactly one 'good'; anything else # stops, with no path that falls through to accept (a switch over no answer at # all would run no clause and return). Name is the signed file's name; # SHA256SUMS is "the release signature". function Test-SignedFile([string]$DataPath, [string]$AscPath, [string]$Name, [string]$HostName) { $data = [IO.File]::ReadAllBytes($DataPath) $asc = [IO.File]::ReadAllText($AscPath) $result = @(Test-PgpSignature $data $asc $Script:Trust) if ($result.Count -eq 1 -and $result[0] -ceq 'good') { return } $what = "the signature of $Name" if ($Name -ceq 'SHA256SUMS') { $what = 'the release signature' } if ($result.Count -eq 1 -and $result[0] -ceq 'unreadable') { Stop-Setup @("$what from $HostName could not be checked.", "$Name.asc is not a signature this installer can read.", 'Files are accepted only with a good signature from the Zecnero', 'release key, never on checksums alone. Nothing was installed.') } Stop-Setup @("$what from $HostName is NOT valid.", "$Name was not signed by the Zecnero release key.", 'Nothing was installed. Do not run these files.', 'Please report this at https://gitlab.com/zecnero or on Discord.') } function Test-ReleaseSignature([string]$Dir, [string]$HostName) { Test-SignedFile (Join-Path $Dir 'SHA256SUMS') (Join-Path $Dir 'SHA256SUMS.asc') 'SHA256SUMS' $HostName } # The SHA-256 of this script as it runs: the file's bytes when it runs from a # file, else (the one-liner, through iex) the text iex was given, as UTF-8 # (the script is plain ASCII, so that is the file's bytes too). function Get-OwnScriptHash { if ($Script:RunAsFile -and $Script:SelfPath -and (Test-Path -LiteralPath $Script:SelfPath)) { return (Get-FileSha256 $Script:SelfPath) } $ast = (Get-Command -Name Invoke-Main -CommandType Function).ScriptBlock.Ast while ($null -ne $ast.Parent) { $ast = $ast.Parent } $bytes = (New-Object Text.UTF8Encoding $false).GetBytes($ast.Extent.Text) return (ConvertTo-Hex ([Security.Cryptography.SHA256]::Create().ComputeHash($bytes))).ToLowerInvariant() } # The installer checks itself against the signed INSTALLERS.SHA256SUMS (its # entry install-native.ps1): an old copy, or one changed on the way, stops # before anything is downloaded. The installed copy (bin\zecnero-miner.ps1) # is checked against its release's SHA256SUMS instead, when it is installed. function Test-InstallerItself([string]$HostName) { if ($Script:RunAsFile -and $Script:SelfPath -and ((Split-Path -Parent $Script:SelfPath) -ieq (Get-Paths).Bin)) { return } $want = Get-SumEntry "$Script:InstallerSums" 'install-native.ps1' if ($null -eq $want) { Stop-Setup @("INSTALLERS.SHA256SUMS from $HostName lists no install-native.ps1,", 'so this installer cannot check itself. Nothing was installed.') } $got = Get-OwnScriptHash if ($got -cne $want) { Stop-Setup @('this install-native.ps1 is not the one the signed INSTALLERS.SHA256SUMS lists.', "expected $want", "got $got", 'It is an old copy, or it was changed on the way. Nothing was installed.', 'Run the one-liner again (it downloads the current one):', " $Script:InstallCommand") } Write-Say ' the installer itself matches the signed INSTALLERS.SHA256SUMS' } # Makes Stage the new bin in one rename; the old bin stays as bin.previous. function Install-Staged([string]$Stage) { $p = Get-Paths if (Test-Path -LiteralPath $p.Previous) { Remove-Item -LiteralPath $p.Previous -Recurse -Force } $had = Test-Path -LiteralPath $p.Bin if ($had) { try { Rename-Item -LiteralPath $p.Bin -NewName 'bin.previous' } catch { Stop-Setup @("could not move the old $($p.Bin) aside.", "$($_.Exception.Message)", 'Stop the miner (zecnero-miner stop) and try again.') } } # An antivirus scan can hold a new file for a moment: try a few times. for ($try = 1; ; $try++) { try { Move-Item -LiteralPath $Stage -Destination $p.Bin break } catch { if ($try -lt 5) { Start-Sleep -Seconds 1; continue } if ($had) { Rename-Item -LiteralPath $p.Previous -NewName 'bin' } Stop-Setup @('could not move the new release into place.', "$($_.Exception.Message)") } } } function Write-Shim { $p = Get-Paths $text = "@echo off`r`n" + "rem Zecnero native pool miner: runs bin\zecnero-miner.ps1 with the command given.`r`n" + "`"%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`" -NoProfile -ExecutionPolicy Bypass -File `"%~dp0bin\zecnero-miner.ps1`" %*`r`n" [IO.File]::WriteAllText($p.Shim, $text, [Text.Encoding]::ASCII) } # Runs a command of the installed bin\zecnero-miner.ps1 in a new PowerShell # (the one just installed and checked, not this copy); its exit code is in # $Script:ChildCode. Call it as a statement, never capture it. function Invoke-Installed([string[]]$Arguments) { $script = Join-Path (Get-Paths).Bin 'zecnero-miner.ps1' $exe = (Get-Process -Id $PID).Path $a = @('-NoProfile', '-ExecutionPolicy', 'Bypass') if (-not (Test-Interactive)) { $a += '-NonInteractive' } $a += @('-File', $script) + $Arguments # Not captured: the new PowerShell writes to this window and reads from it. & $exe @a $Script:ChildCode = $LASTEXITCODE } function Remove-Leftovers { $p = Get-Paths if (-not (Test-Path -LiteralPath $p.Download)) { return } Get-ChildItem -LiteralPath $p.Download -Force | Where-Object { $_.LastWriteTime -lt (Get-Date).AddMinutes(-10) } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } # ---------------------------------------------------------------- the pool and xmrig's config function Get-PoolWeb { $w = "$env:ZECNERO_POOL_WEB".Trim().TrimEnd('/') if ($w -eq '') { return $Script:PoolWebDefault } if ($w -match '^https://[A-Za-z0-9.-]+(:\d+)?$' -or $w -match '^http://127\.0\.0\.1:\d+$') { return $w } Stop-Setup @('ZECNERO_POOL_WEB must be https://..., or http://127.0.0.1:PORT', "It is: $w") } # The pool's /api/stats "config": its stratum ports (with and without TLS) and # algorithm. The host stays stratum.zecnero.org whatever the API says. function Get-PoolEndpoints { $r = [pscustomobject]@{ Tls = $Script:PoolTlsPort; Plain = $Script:PoolPlainPort; Algo = $Script:DefaultAlgo; From = 'defaults' } try { $params = Get-WebParams "$(Get-PoolWeb)/api/stats" 10 $previous = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { $stats = Invoke-RestMethod @params } finally { $ProgressPreference = $previous } $cfg = $stats.config if ($null -eq $cfg) { return $r } foreach ($port in @($cfg.ports)) { $n = 0 if (-not [int]::TryParse("$($port.port)", [ref]$n) -or $n -lt 1 -or $n -gt 65535) { continue } if ($port.tls -eq $true) { $r.Tls = $n } else { $r.Plain = $n } } if ("$($cfg.algorithm)" -in $Script:KnownAlgos) { $r.Algo = "$($cfg.algorithm)" } $r.From = 'pool' } catch { } return $r } # Whether the installed xmrig-zecnero.exe names Algo (it lists the algorithms # it knows as plain strings). True when there is no program to look at yet. function Test-MinerKnowsAlgo([string]$Algo) { $exe = Get-MinerExe if (-not (Test-Path -LiteralPath $exe)) { return $true } try { $bytes = [IO.File]::ReadAllBytes($exe) } catch { return $true } $text = [Text.Encoding]::GetEncoding(28591).GetString($bytes) return $text.Contains($Algo) } function Get-Worker { $s = Read-Settings $w = "$($s.Worker)" if ($w -eq '') { $w = $env:COMPUTERNAME } $w = ($w -replace '[^A-Za-z0-9_-]', '') if ($w.Length -gt 20) { $w = $w.Substring(0, 20) } if ($w -eq '') { $w = 'windows' } return $w } function Test-SaplingAddress([string]$Address) { return ($Address -cmatch ('^' + $Script:SaplingHrp + '1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{75}$')) } # Writes config\xmrig.json from miner.json: the Sapling login with this # computer's worker name, TLS on the pool's TLS port first and plain as the # fallback, huge pages when Windows allows them, no 1 GB pages, and no MSR # reads or writes (no driver is loaded). The status API listens on 127.0.0.1 # only, with a token. function Write-XmrigConfig { $p = Get-Paths $s = Read-Settings $login = "$($s.PoolLogin)" if (-not (Test-SaplingAddress $login)) { Stop-Setup @('there is no Sapling pool login yet.', 'Set one with: zecnero-miner pool-login new (or pool-login ntestsapling1...)') } if (-not $s.ApiPort) { $s.ApiPort = $Script:DefaultApiPort } if (-not $s.ApiToken) { $s.ApiToken = New-Token } if ($null -eq $s.Tls) { $s.Tls = $true } Save-Settings $s $ep = Get-PoolEndpoints if (-not (Test-MinerKnowsAlgo $ep.Algo)) { Stop-Setup @("the pool mines $($ep.Algo), and the installed miner ($(Get-InstalledVersion)) cannot.", 'rx/zecnero2 (RandomX v2) is the proof of work from NU2 on; the miner of', 'the releases before NU2 knows only rx/zecnero. Install the current', 'release, checked as before:', ' zecnero-miner update') } $user = "$login.$(Get-Worker)" $pools = @() if ($s.Tls) { $pools += [ordered]@{ url = "$($Script:PoolHost):$($ep.Tls)"; algo = $ep.Algo; user = $user; pass = 'x'; keepalive = $true; tls = $true } } $pools += [ordered]@{ url = "$($Script:PoolHost):$($ep.Plain)"; algo = $ep.Algo; user = $user; pass = 'x'; keepalive = $true; tls = $false } $cpu = [ordered]@{ enabled = $true; 'huge-pages' = $true; 'huge-pages-jit' = $false; 'hw-aes' = $null; priority = $null; 'max-threads-hint' = 100; yield = $true } $threads = 0 if ([int]::TryParse("$($s.Threads)", [ref]$threads) -and $threads -gt 0) { $cpu.rx = @(1..$threads | ForEach-Object { -1 }) } New-Item -ItemType Directory -Force -Path $p.Logs | Out-Null $config = [ordered]@{ autosave = $false; background = $false; colors = $true 'donate-level' = 0; 'donate-over-proxy' = 0 'log-file' = $p.Log 'print-time' = 60; 'health-print-time' = 0; retries = 5; 'retry-pause' = 5 http = [ordered]@{ enabled = $true; host = '127.0.0.1'; port = [int]$s.ApiPort; 'access-token' = "$($s.ApiToken)"; restricted = $true } randomx = [ordered]@{ init = -1; mode = 'auto'; '1gb-pages' = $false; rdmsr = $false; wrmsr = $false; cache_qos = $false; numa = $true } cpu = $cpu opencl = $false; cuda = $false pools = $pools } Write-Utf8File $p.Xmrig ($config | ConvertTo-Json -Depth 6) return $ep } # ---------------------------------------------------------------- the wallet and the pool login function Get-WalletExe { return (Join-Path (Get-Paths).Bin 'zecnero-wallet.exe') } function Read-NewPassphrase { if ($env:ZECNERO_WALLET_PASSPHRASE) { return $env:ZECNERO_WALLET_PASSPHRASE } if (-not (Test-Interactive)) { Stop-Setup @('making a wallet needs a window, to set its passphrase and show its seed.', 'Run this in PowerShell: zecnero-miner pool-login new') } while ($true) { $a = Read-Secret 'Choose a passphrase for the new wallet (it does not show while you type)' $b = Read-Secret 'Type it again' if ($a -eq '') { Write-Say 'The passphrase cannot be empty.' } elseif ($a -ne $b) { Write-Say 'The two did not match. Once more.' } else { return $a } } } function Write-SeedFile([string]$Words, [string]$Address) { $p = Get-Paths if (Test-Path -LiteralPath $p.Seed) { Rename-Item -LiteralPath $p.Seed -NewName ("POOL-WALLET-SEED-$(Get-Date -Format yyyyMMdd-HHmmss).txt") } $text = @" Zecnero Testnet wallet for pool payouts Created $((Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm')) UTC Sapling address (your pool login; the pool pays here): $Address Seed (24 words): $Words SAVE THIS. IT IS THE ONLY COPY. The 24 words are the only backup of this wallet. Anyone who has them can spend what the pool pays you. Keep a copy somewhere safe (a password manager, or on paper), and never share them. The wallet file is $(Get-WalletFile) and it is locked with the passphrase you chose. To see what the pool paid you: "$(Get-WalletExe)" balance To restore the wallet elsewhere: zecnero-wallet restore, and type the 24 words. Wallet downloads: https://zecnero.org "@ Write-Utf8File $p.Seed ($text -replace "`r?`n", "`r`n") } function Show-SeedAndConfirm([string]$Words, [string]$Address) { $p = Get-Paths if ($env:ZECNERO_ASSUME_SAVED -eq '1') { Write-Say "Your wallet's seed was saved to $($p.Seed) (ZECNERO_ASSUME_SAVED=1, not shown)." return } if (-not (Test-Interactive)) { Stop-Setup @('this step needs a window, to show you your new wallet''s seed.', "Your seed is in $($p.Seed)") } $w = $Words -split ' ' Write-Say '' Write-Box @('SAVE THIS. IT IS THE ONLY COPY.', '', 'Your pool login (the pool pays this Sapling address):', " $Address", '', 'Your wallet''s 24-word seed (the only way to spend what it is paid):', " $($w[0..7] -join ' ')", " $($w[8..15] -join ' ')", " $($w[16..23] -join ' ')", '', 'If you lose the seed, your pool payouts are gone for good.', 'Anyone who sees the seed can take them. Never share it.', '', "A copy is in $($p.Seed)", 'Copy it to a password manager or write it down now.') 'Yellow' Write-Say '' while ($true) { $answer = Read-Host 'Type SAVED and press Enter once you have saved it' if ("$answer".Trim() -match '^saved$') { break } Write-Say 'Please type SAVED (or press Ctrl+C to stop; it stays in the file).' } # Take it off the screen and, where the terminal allows, out of the scrollback. Clear-Host Write-Host "$([char]27)[3J" -NoNewline Write-Say "Thanks. Your seed is also in $($p.Seed)" } # Makes a wallet with zecnero-wallet.exe and returns its Sapling address. An # existing zecnero-wallet Testnet wallet is used instead (its passphrase asked). function New-PoolWallet { $exe = Get-WalletExe if (-not (Test-Path -LiteralPath $exe)) { Stop-Setup @('this install has no zecnero-wallet.exe, so it cannot make a wallet.', 'Run: zecnero-miner update') } $wallet = Get-WalletFile $words = '' if (Test-Path -LiteralPath $wallet) { Write-Say "You already have a zecnero-wallet Testnet wallet ($wallet)." Write-Say 'Using its Sapling address.' $pass = $env:ZECNERO_WALLET_PASSPHRASE if (-not $pass) { if (-not (Test-Interactive)) { Stop-Setup @('this needs a window, for the wallet''s passphrase.') } $pass = Read-Secret 'That wallet''s passphrase' } } else { $pass = Read-NewPassphrase New-Item -ItemType Directory -Force -Path (Split-Path -Parent $wallet) | Out-Null $r = Invoke-Program $exe @('--network', 'testnet', '--wallet', $wallet, 'new') @{ ZWALLET_PASSPHRASE = $pass } if ($r.Code -ne 0) { Stop-Setup @('zecnero-wallet could not make the wallet:', "$($r.Out)$($r.Err)".Trim()) } $words = ([regex]::Matches($r.Out, '(?m)\b\d+\.\s+([a-z]+)') | ForEach-Object { $_.Groups[1].Value }) -join ' ' $r = $null if (@($words -split ' ').Count -ne 24) { Stop-Setup @("zecnero-wallet made the wallet ($wallet), but its seed could not be read here.", 'Its seed was shown only once; delete that file and run: zecnero-miner pool-login new') } } $r = Invoke-Program $exe @('--network', 'testnet', '--wallet', $wallet, 'z-address') @{ ZWALLET_PASSPHRASE = $pass } $pass = $null $address = (("$($r.Out)".Trim() -split "`r?`n") | Select-Object -Last 1).Trim() if ($words -ne '') { Write-SeedFile $words $address Show-SeedAndConfirm $words $address $words = $null } if ($r.Code -ne 0 -or -not (Test-SaplingAddress $address)) { Stop-Setup @("zecnero-wallet did not give a Sapling address (got: $address$($r.Err)).", 'Check the passphrase, then run: zecnero-miner pool-login new') } return $address } function Set-PoolLogin([string]$Login) { $a = $Login.Trim() if ($a -eq 'new') { $a = New-PoolWallet } if (-not (Test-SaplingAddress $a)) { if ($a -match '^nm') { Stop-Setup @("the pool cannot pay a transparent address ($a).", 'Testnet pays shielded addresses only. Log in with a Sapling address', '(ntestsapling1...), or make one: zecnero-miner pool-login new') } Stop-Setup @("that is not a Zecnero Testnet Sapling address: $a", 'It starts with ntestsapling1 and is 88 characters long. Or make one: zecnero-miner pool-login new') } Set-Setting 'PoolLogin' $a Write-Say "Pool login: $a (Sapling)" } function Request-PoolLogin { if ("$env:ZECNERO_POOL_LOGIN".Trim() -ne '') { Set-PoolLogin "$env:ZECNERO_POOL_LOGIN"; return } if (-not (Test-Interactive)) { Stop-Setup @('pool mining needs a Sapling address to log in with, and there is no window to ask for one.', 'Set $env:ZECNERO_POOL_LOGIN to your address or to new, or run: zecnero-miner pool-login new') } Write-Say '' Write-Say 'The pool pays shielded (Sapling) addresses only, so pool mining needs one.' Write-Say '' Write-Say ' 1) Make a new wallet with zecnero-wallet and use its Sapling address (easiest)' Write-Say ' 2) Use a Sapling address I already have (ntestsapling1...)' while ($true) { $ans = "$(Read-Host 'Choose 1 or 2 [1]')".Trim() if ($ans -eq '' -or $ans -eq '1') { Set-PoolLogin 'new'; return } if ($ans -eq '2') { $a = "$(Read-Host 'Paste your Sapling address')".Trim() if (Test-SaplingAddress $a) { Set-PoolLogin $a; return } Write-Say 'That is not a Testnet Sapling address (ntestsapling1..., 88 characters). Try again.' } } } # ---------------------------------------------------------------- register a payout address # Signs "Zecnero pool payout address\n\n" with an nm... key # (Bitcoin signmessage with the "Zecnero Signed Message:\n" prefix, done by # zecnero-wallet.exe sign-message), and posts {address, sapling, signature} # to the pool, which then pays what it owes the nm... address to the Sapling # one. The key goes to zecnero-wallet in that one process's environment and is # never printed or sent. function Get-PayoutMessage([string]$Address, [string]$Sapling) { return "Zecnero pool payout address`n$Address`n$Sapling" } function Read-KeyFile([string]$Path) { $text = Get-Content -Raw -LiteralPath $Path $wif = $null $address = $null foreach ($line in ($text -split "`r?`n")) { if (-not $wif -and $line -match '^\s*private key:\s*(\S+)\s*$') { $wif = $Matches[1] } if (-not $address -and $line -match '^\s*address:\s*(nm\S+)\s*$') { $address = $Matches[1] } } return [pscustomobject]@{ Wif = $wif; Address = $address } } function Send-PoolRegistration([string]$Body) { $params = Get-WebParams "$(Get-PoolWeb)/api/register" 20 $params.Method = 'Post' $params.ContentType = 'application/json' $params.Body = $Body try { $r = Invoke-WebRequest @params return [pscustomobject]@{ Code = [int]$r.StatusCode; Body = "$($r.Content)" } } catch { $ex = $_.Exception $code = 0 $body = '' if ($null -ne $ex.Response -and $null -ne $ex.Response.StatusCode) { $code = [int]$ex.Response.StatusCode } # PowerShell keeps the body of an HTTP error in ErrorDetails. $body = "$($_.ErrorDetails.Message)" if ($body -eq '' -and $ex -is [Net.WebException] -and $ex.Response) { try { $body = (New-Object IO.StreamReader ($ex.Response.GetResponseStream())).ReadToEnd() } catch { } } if ($code -eq 0) { Stop-Setup @("could not reach $(Get-PoolWeb): $(Get-WebWhy $_)", 'Nothing was registered. Try again later.') } return [pscustomobject]@{ Code = $code; Body = $body } } } function Invoke-RegisterPayout([string[]]$Rest) { $sapling = '' $keyFile = '' for ($i = 0; $i -lt $Rest.Count; $i++) { if ($Rest[$i] -eq '--key-file' -and $i + 1 -lt $Rest.Count) { $keyFile = $Rest[$i + 1]; $i++ } elseif ($sapling -eq '') { $sapling = $Rest[$i].Trim() } } if ($sapling -eq '') { Stop-Setup @('use: zecnero-miner register-payout ntestsapling1... [--key-file MINING-KEY.txt]', 'It asks the pool to pay what it owes your nm... mining address to that Sapling address.') } if (-not (Test-SaplingAddress $sapling)) { Stop-Setup @("that is not a Zecnero Testnet Sapling address: $sapling", 'It starts with ntestsapling1 and is 88 characters long. To make one: zecnero-miner pool-login new') } $exe = Get-WalletExe if (-not (Test-Path -LiteralPath $exe)) { Stop-Setup @('this install has no zecnero-wallet.exe to sign with.', 'Run: zecnero-miner update') } if ($keyFile -eq '' -and (Test-Path -LiteralPath (Get-Paths).Key)) { $keyFile = (Get-Paths).Key } $key = [pscustomobject]@{ Wif = $null; Address = $null } if ($keyFile -ne '') { if (-not (Test-Path -LiteralPath $keyFile)) { Stop-Setup @("there is no key file $keyFile.") } $key = Read-KeyFile $keyFile if (-not $key.Wif) { Stop-Setup @("$keyFile has no private key in it.") } } elseif (Test-Interactive) { Write-Say 'No MINING-KEY.txt here. Paste the private key of your nm... mining address' Write-Say '(from MINING-KEY.txt; it does not show while you type):' $key.Wif = (Read-Secret 'Private key').Trim() } else { Stop-Setup @('there is no mining key to sign with.', 'Use --key-file with your MINING-KEY.txt (in WSL: \\wsl$\Ubuntu\home\\zecnero\MINING-KEY.txt).') } # First sign a probe, only to learn the address the key belongs to. if (-not $key.Address) { $probe = Invoke-Program $exe @('--network', 'testnet', 'sign-message') @{ ZWALLET_WIF = $key.Wif } 'x' if ($probe.Code -ne 0) { Stop-Setup @('zecnero-wallet could not use that private key.', "$($probe.Err)".Trim()) } $key.Address = "$(($probe.Out | ConvertFrom-Json).address)" } if ($key.Address -notmatch '^nm[1-9A-HJ-NP-Za-km-z]+$') { Stop-Setup @("the key is not for an nm... address (got $($key.Address)).") } $message = Get-PayoutMessage $key.Address $sapling $pool = Get-PoolWeb Write-Say '' Write-Say 'This signs the message below with the private key of your mining address,' Write-Say 'on this computer, and sends the pool only the message and the signature.' Write-Say 'Your private key never leaves this computer.' Write-Say '' Write-Say "Mining address: $($key.Address)" Write-Say "Pool: $pool/api/register" Write-Say '' Write-Say 'Message (exactly as signed):' foreach ($line in ($message -split "`n")) { Write-Say " | $line" } Write-Say '' Write-Say "The pool will pay what it owes $($key.Address)" Write-Say "to $sapling" Write-Say '' $ans = "$env:ZECNERO_REGISTER_CONFIRM" if ($ans -eq '') { if (-not (Test-Interactive)) { Stop-Setup @('this needs a window, to confirm. (Or set $env:ZECNERO_REGISTER_CONFIRM = "REGISTER".)') } $ans = Read-Host 'Type REGISTER and press Enter to sign and send it (anything else stops)' } if ("$ans".Trim() -cne 'REGISTER') { Stop-Setup @('stopped. Nothing was signed or sent.') } $r = Invoke-Program $exe @('--network', 'testnet', 'sign-message') @{ ZWALLET_WIF = $key.Wif } $message $key.Wif = $null if ($r.Code -ne 0) { Stop-Setup @('zecnero-wallet could not sign the message.') } $out = $r.Out | ConvertFrom-Json if ("$($out.address)" -ne $key.Address) { Stop-Setup @("the signature is for $($out.address), not $($key.Address). Nothing was sent.") } $sig = "$($out.signature)" if ($sig -cnotmatch '^[A-Za-z0-9+/]{86}[AEIMQUYcgkosw048]=$') { Stop-Setup @('the signer gave no 65-byte signature. Nothing was sent.') } $body = '{"address":"' + $key.Address + '","sapling":"' + $sapling + '","signature":"' + $sig + '"}' $resp = Send-PoolRegistration $body if ($resp.Code -eq 200) { Write-Say "Registered: the pool pays what it owes $($key.Address) to $sapling" Write-Say "Check it on $pool/miner/$($key.Address)" return } $err = '' try { $err = "$(($resp.Body | ConvertFrom-Json).error)" } catch { $err = "$($resp.Body)" } Stop-Setup @("the pool refused it: $err", "(HTTP $($resp.Code)). Nothing was registered.") } # ---------------------------------------------------------------- huge pages: the "Lock pages in memory" right # # This script never gives or removes the right itself: that needs an # administrator, and the miner never asks for one. It reads whether this # sign-in has the right, and shows the steps for you to do it yourself. # active: this sign-in has the right; missing: it does not (never given, or # given since the last sign-in: Windows applies it from the next sign-in). function Get-LockPagesState { if ($env:ZECNERO_PS_FAKE_LOCK_PAGES) { return $env:ZECNERO_PS_FAKE_LOCK_PAGES } try { $r = Invoke-Program (Join-Path $env:SystemRoot 'System32\whoami.exe') @('/priv') if ($r.Out -match 'SeLockMemoryPrivilege') { return 'active' } } catch { } return 'missing' } # Windows Home has no Local Security Policy (secpol.msc). function Test-HasSecpol { if ($env:ZECNERO_PS_FAKE_SECPOL) { return ($env:ZECNERO_PS_FAKE_SECPOL -eq 'yes') } return (Test-Path -LiteralPath (Join-Path $env:SystemRoot 'System32\secpol.msc')) } function Get-HugePagesSteps([bool]$Add) { $account = "$env:USERDOMAIN\$env:USERNAME" $exe = Get-MinerExe if (-not $Add) { return @('To take the "Lock pages in memory" right away again (you do this,', 'as an administrator; this script never changes it):', '', ' Windows Pro, Enterprise or Education:', ' 1. Press Windows+R, type secpol.msc, press Enter (allow the prompt).', ' 2. Local Policies > User Rights Assignment > Lock pages in memory.', " 3. Select $account, Remove, OK.", ' 4. Sign out of Windows and back in.', '', ' Windows Home has no screen for user rights. The right only lets your', ' own programs keep memory they lock in RAM, and is safe to leave. To', ' remove it anyway (with secedit, as an administrator), see', ' https://zecnero.org/mine#windows-huge-pages') } $lines = @('Huge pages, for faster mining (optional): the steps', '', 'RandomX mines faster (often 20-30%) when Windows lets the miner lock', 'large (2 MB) memory pages. Windows allows that only to accounts with the', '"Lock pages in memory" right, which no account has by default. Giving it', 'needs an administrator, once. This script never does it for you and', 'never asks for administrator rights: you do the steps below yourself.', '') if (Test-HasSecpol) { $lines += @(' Windows Pro, Enterprise or Education (this Windows has secpol.msc):', ' 1. Press Windows+R, type secpol.msc, press Enter. Windows asks for', ' administrator permission: allow it (or type an administrator''s', ' password).', ' 2. Open Local Policies > User Rights Assignment, and double-click', ' "Lock pages in memory".', ' 3. Add User or Group..., type the account that mines:', " $account", ' then Check Names, OK, and OK again.', '') } else { $lines += @(' Windows Home (no secpol.msc here): the miner itself can give the', ' right to the account that runs it, when it runs once as administrator.', ' 1. Open the Start menu, type PowerShell, right-click Windows', ' PowerShell, "Run as administrator", allow the prompt. Do this from', " the account that mines ($account): if Windows asks for", ' ANOTHER account''s password, that account gets the right instead', ' and this one does not; use Pro''s steps or skip huge pages.', ' 2. In that window, run (one line):', " & '$exe' --bench=1M -a rx/zecnero --no-color", ' It prints "Huge pages support was successfully enabled, but', ' reboot required to use it" near the top. Then press Ctrl+C and', ' close the administrator window.', '') } $lines += @(' Then, either way:', ' 4. Sign out of Windows and back in (or restart): Windows applies the', ' right only from the next sign-in.', ' 5. zecnero-miner huge-pages must say "this account has the right";', ' after zecnero-miner start, zecnero-miner status shows', ' "Huge pages: in use".', '', 'Without it the miner works the same, only slower. Undo: zecnero-miner', 'huge-pages off shows the steps. Details:', 'https://zecnero.org/mine#windows-huge-pages') return $lines } # Action: status (what this sign-in has), on (the steps to give the right), # off (the steps to take it away), or setup (a short note during the setup). # None of them changes anything. function Invoke-HugePages([string]$Action) { $state = Get-LockPagesState switch ($Action) { 'on' { if ($state -eq 'active') { Write-Say 'Huge pages: this account has the "Lock pages in memory" right, so the miner can use them. Nothing to do.'; return } Write-Box (Get-HugePagesSteps $true) 'Cyan' } 'off' { if ($state -ne 'active') { Write-Say 'This sign-in does not have the "Lock pages in memory" right (if you just took it away, that holds from your next sign-in).' } Write-Box (Get-HugePagesSteps $false) 'Cyan' } 'setup' { if ($state -eq 'active') { Write-Say 'Huge pages: this account has the "Lock pages in memory" right; the miner uses them.'; return } Write-Say '' Write-Box @('Optional: huge pages, for faster mining', '', 'This account does not have the "Lock pages in memory" right, so the', 'miner runs without huge pages (slower, otherwise the same). Giving', 'it needs an administrator, once, and a sign-out; this installer', 'never does that itself. The steps, for you to do if you want:', ' zecnero-miner huge-pages on') 'Cyan' } default { if ($state -eq 'active') { Write-Say 'Lock pages in memory: this account has the right (huge pages allowed).' } else { Write-Say 'Lock pages in memory: not in this sign-in (no huge pages). The steps: zecnero-miner huge-pages on' } } } } # ---------------------------------------------------------------- shortcuts and autostart function Get-ProgramsFolder { return (Join-Path $env:APPDATA 'Microsoft\Windows\Start Menu\Programs') } function Get-StartupFolder { return (Join-Path (Get-ProgramsFolder) 'Startup') } function Get-StartMenuFolder { return (Join-Path (Get-ProgramsFolder) 'Zecnero') } function Get-AutostartLink { return (Join-Path (Get-StartupFolder) 'Zecnero miner.lnk') } # The desktop, or $null in test mode without ZECNERO_TEST_DESKTOP. function Get-DesktopFolder { if (Test-TestMode) { if ($env:ZECNERO_TEST_DESKTOP) { return $env:ZECNERO_TEST_DESKTOP } return $null } return [Environment]::GetFolderPath('Desktop') } function New-Shortcut([string]$Path, [string]$Arguments, [string]$Description, [int]$WindowStyle = 1) { $p = Get-Paths if (Test-DryRun) { Write-Say "[dry run] would add the shortcut $Path (zecnero-miner $Arguments)"; return } New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Path) | Out-Null $shell = New-Object -ComObject WScript.Shell $link = $shell.CreateShortcut($Path) $link.TargetPath = $p.Shim $link.Arguments = $Arguments $link.WorkingDirectory = $p.Home $link.Description = $Description $link.WindowStyle = $WindowStyle $link.Save() } function Set-Shortcuts([bool]$On, [bool]$Desktop) { $menu = Get-StartMenuFolder $desk = Get-DesktopFolder if (-not $On) { if (Test-DryRun) { Write-Say "[dry run] would remove $menu and the desktop shortcut"; return } Remove-Item -LiteralPath $menu -Recurse -Force -ErrorAction SilentlyContinue if ($desk) { Remove-Item -LiteralPath (Join-Path $desk 'Zecnero miner.lnk') -Force -ErrorAction SilentlyContinue } Write-Say 'Removed the Zecnero shortcuts.' return } New-Shortcut (Join-Path $menu 'Zecnero miner.lnk') 'start --pause-on-error' 'Start mining Zecnero on the pool' New-Shortcut (Join-Path $menu 'Zecnero miner status.lnk') 'status --pause' 'Zecnero miner: hashrate, shares and pool' New-Shortcut (Join-Path $menu 'Stop Zecnero miner.lnk') 'stop --pause' 'Stop mining Zecnero' New-Shortcut (Join-Path $menu 'Update Zecnero miner.lnk') 'update --pause' 'Install the latest Zecnero miner release' Write-Say "Added Start menu shortcuts in $menu" if ($Desktop) { if ($desk) { New-Shortcut (Join-Path $desk 'Zecnero miner.lnk') 'start --pause-on-error' 'Start mining Zecnero on the pool' Write-Say 'Added a desktop shortcut: Zecnero miner.' } else { Write-Say 'No desktop folder in test mode: no desktop shortcut.' } } } function Set-Autostart([bool]$On) { $link = Get-AutostartLink if ($On) { New-Shortcut $link 'start --minimized --pause-on-error' 'Start mining Zecnero when you sign in' 7 if (-not (Test-DryRun)) { Write-Say "Autostart on: mining starts (minimized) when you sign in to Windows. To stop that: zecnero-miner autostart off" } } else { if (Test-DryRun) { Write-Say "[dry run] would remove $link"; return } Remove-Item -LiteralPath $link -Force -ErrorAction SilentlyContinue Write-Say 'Autostart off: mining does not start with Windows.' } } # ---------------------------------------------------------------- the miner process function Get-MinerExe { return (Join-Path (Get-Paths).Bin 'xmrig-zecnero.exe') } function Get-MinerProcess { $p = Get-Paths $exe = Get-MinerExe if (Test-Path -LiteralPath $p.Pid) { $id = 0 if ([int]::TryParse((Get-Content -Raw -LiteralPath $p.Pid).Trim(), [ref]$id)) { $proc = Get-Process -Id $id -ErrorAction SilentlyContinue if ($proc -and $proc.Path -and ($proc.Path -ieq $exe)) { return $proc } } } return (Get-Process -Name 'xmrig-zecnero' -ErrorAction SilentlyContinue | Where-Object { $_.Path -ieq $exe } | Select-Object -First 1) } function Start-Miner([bool]$Minimized) { $p = Get-Paths if (-not (Test-Installed)) { Stop-Setup @("the miner is not installed in $($p.Home).", "Install it with: $Script:InstallCommand") } $running = Get-MinerProcess if ($running) { Write-Say "The miner is already running (process $($running.Id))."; return } if (-not (Test-Path -LiteralPath $p.Xmrig) -or ((Test-Path -LiteralPath $p.Settings) -and (Get-Item -LiteralPath $p.Settings).LastWriteTime -gt (Get-Item -LiteralPath $p.Xmrig).LastWriteTime)) { [void](Write-XmrigConfig) } New-Item -ItemType Directory -Force -Path $p.Logs, $p.Run | Out-Null if ((Test-Path -LiteralPath $p.Log) -and (Get-Item -LiteralPath $p.Log).Length -gt $Script:LogMaxBytes) { Move-Item -LiteralPath $p.Log -Destination "$($p.Log).old" -Force } $style = 'Normal' if ($Minimized) { $style = 'Minimized' } # The tests start a stand-in miner: no window on the desktop. if (Test-TestMode) { $style = 'Hidden' } try { $proc = Start-Process -FilePath (Get-MinerExe) -ArgumentList @("--config=`"$($p.Xmrig)`"") -WorkingDirectory $p.Bin -WindowStyle $style -PassThru } catch { Show-DefenderStop 'xmrig-zecnero.exe' "It could not be started: $($_.Exception.Message)" } Write-Utf8File $p.Pid "$($proc.Id)" Start-Sleep -Seconds 2 if ($proc.HasExited) { if (-not (Test-Path -LiteralPath (Get-MinerExe))) { Show-DefenderStop 'xmrig-zecnero.exe' 'It was removed when it started.' } $tail = @() if (Test-Path -LiteralPath $p.Log) { $tail = @(Get-Content -LiteralPath $p.Log -Tail 5) } Stop-Setup (@("the miner stopped right after it started (exit code $($proc.ExitCode)).") + $tail) } Write-Say "Mining: xmrig-zecnero is running (process $($proc.Id)) in its own window." Write-Say 'To check on it: zecnero-miner status. To stop: zecnero-miner stop, or close its window.' } function Stop-Miner { $proc = Get-MinerProcess $p = Get-Paths if (-not $proc) { Write-Say 'The miner is not running.'; Remove-Item -LiteralPath $p.Pid -Force -ErrorAction SilentlyContinue; return } Stop-Process -Id $proc.Id -Force try { $proc.WaitForExit(10000) | Out-Null } catch { } Remove-Item -LiteralPath $p.Pid -Force -ErrorAction SilentlyContinue Write-Say 'Stopped the miner.' } function Get-MinerSummary { $s = Read-Settings if (-not $s.ApiPort -or -not $s.ApiToken) { return $null } try { return Invoke-RestMethod -Uri "http://127.0.0.1:$($s.ApiPort)/2/summary" -Headers @{ Authorization = "Bearer $($s.ApiToken)" } -TimeoutSec 3 -UseBasicParsing } catch { return $null } } function Format-Rate($Value) { if ($null -eq $Value) { return 'n/a' } $v = [double]$Value if ($v -ge 1000) { return ('{0:N2} kH/s' -f ($v / 1000)) } return ('{0:N1} H/s' -f $v) } function Show-Status { $p = Get-Paths $s = Read-Settings $version = Get-InstalledVersion if (-not $version) { Write-Say "Not installed in $($p.Home). Install with: $Script:InstallCommand"; return } Write-Say "Zecnero native pool miner $version ($($p.Home))" $proc = Get-MinerProcess if ($proc) { $sum = Get-MinerSummary if ($sum) { $h = @($sum.hashrate.total) Write-Say ("Miner: running (process {0}), {1} (10 s), {2} (60 s)" -f $proc.Id, (Format-Rate $h[0]), (Format-Rate $h[1])) Write-Say ("Shares: {0} accepted of {1}" -f $sum.results.shares_good, $sum.results.shares_total) Write-Say ("Pool: {0} ({1}), algorithm {2}" -f $sum.connection.pool, $(if ($sum.connection.tls) { "TLS $($sum.connection.tls)" } else { 'plain' }), $sum.algo) $hp = @($sum.hugepages) if ($hp.Count -eq 2 -and [int]$hp[1] -gt 0 -and [int]$hp[0] -eq [int]$hp[1]) { Write-Say "Huge pages: in use ($($hp[0])/$($hp[1]))" } elseif ($hp.Count -eq 2) { Write-Say "Huge pages: not in use ($($hp[0])/$($hp[1]))" } } else { Write-Say "Miner: running (process $($proc.Id)); its status API does not answer yet." } } else { Write-Say 'Miner: not running. Start it with: zecnero-miner start' } if ((Get-LockPagesState) -eq 'active') { Write-Say 'Lock pages: this account has the right (huge pages allowed).' } else { Write-Say 'Lock pages: not in this sign-in (no huge pages). The steps: zecnero-miner huge-pages on' } if ($s.PoolLogin) { Write-Say "Login: $($s.PoolLogin) (worker $(Get-Worker))" Write-Say "Your page: $(Get-PoolWeb)/miner/$($s.PoolLogin)" } else { Write-Say 'Login: none yet. Set one with: zecnero-miner pool-login new' } if (Test-Path -LiteralPath (Get-AutostartLink)) { Write-Say 'Autostart: on' } else { Write-Say 'Autostart: off' } } # ---------------------------------------------------------------- install, setup, update function Get-WindowsProblem { if ($PSVersionTable.PSVersion.Major -lt 5) { return "this PowerShell is too old ($($PSVersionTable.PSVersion)); Windows 10 and 11 come with 5.1." } $arch = $env:PROCESSOR_ARCHITECTURE if ($env:PROCESSOR_ARCHITEW6432) { $arch = $env:PROCESSOR_ARCHITEW6432 } if (-not [Environment]::Is64BitOperatingSystem -or $arch -ne 'AMD64') { return "this processor is not supported ($arch). The Windows miner is built for 64-bit Intel and AMD processors." } return $null } function Invoke-Install { Write-Say 'Zecnero Testnet pool miner for Windows (native, no WSL), beta' Write-Say '' $problem = Get-WindowsProblem if ($problem) { Stop-Setup @($problem) } try { $gb = [Math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory / 1GB, 1) Write-Say "Windows $([Environment]::OSVersion.Version), $gb GB memory" if ($gb -lt 4) { Write-Say 'Note: RandomX mining needs about 2.5 GB of free memory; with less than 4 GB it runs slowly or not at all.' } } catch { } Write-Say 'This sets up POOL mining: the pool pays you, and no node has to sync.' Write-Say "For SOLO mining with your own node, use WSL instead: $Script:WslCommand" Write-Say '' $p = Get-Paths New-Item -ItemType Directory -Force -Path $p.Home | Out-Null Remove-Leftovers Initialize-Trust $version = Get-LatestVersion Write-Say " INSTALLERS.SHA256SUMS: signature OK (Zecnero release key $($Script:Trust.Fpr)); LATEST is $version" Test-InstallerItself (Get-HostName (Get-Base)) $installed = Get-InstalledVersion if ($installed -eq $version -and (Test-Installed)) { Write-Say "$version is installed already in $($p.Bin)." } else { Write-Say "Downloading $version from $(Get-HostName (Get-Base)) and checking it:" $stage = Get-VerifiedRelease $version if (Get-MinerProcess) { Stop-Miner } Install-Staged $stage Write-Say "Installed $version in $($p.Bin)." } Write-Shim Invoke-Installed @('setup') if ($Script:ChildCode -ne 0) { $Script:ExitCode = $Script:ChildCode } } function Invoke-Setup { $p = Get-Paths if (-not (Test-Installed)) { Stop-Setup @("the miner is not installed in $($p.Home).", "Install it with: $Script:InstallCommand") } New-Item -ItemType Directory -Force -Path $p.Conf, $p.Logs, $p.Run | Out-Null Write-Shim $s = Read-Settings if (-not $s.ApiToken) { $s.ApiToken = New-Token } if (-not $s.ApiPort) { $s.ApiPort = $Script:DefaultApiPort } if ($null -eq $s.Tls) { $s.Tls = $true } if ("$env:ZECNERO_WORKER".Trim() -ne '') { $s.Worker = "$env:ZECNERO_WORKER".Trim() } Save-Settings $s if ("$env:ZECNERO_POOL_LOGIN".Trim() -ne '' -or -not (Test-SaplingAddress "$($s.PoolLogin)")) { Request-PoolLogin } else { Write-Say "Pool login: $($s.PoolLogin) (Sapling). To change it: zecnero-miner pool-login" } $ep = Write-XmrigConfig Write-Say "Wrote $($p.Xmrig): $($Script:PoolHost) ports $($ep.Tls) (TLS) and $($ep.Plain), $($ep.Algo) (from the $($ep.From)), worker $(Get-Worker)" $s = Read-Settings Invoke-HugePages 'setup' if (-not $s.ShortcutsAsked) { Write-Say '' $menu = Get-Answer 'ZECNERO_SHORTCUTS' 'Add Zecnero miner to the Start menu?' $true $false $desk = $false if ($menu) { $desk = Get-Answer 'ZECNERO_SHORTCUTS' 'Add a desktop shortcut too?' $false $false } if ($menu) { Set-Shortcuts $true $desk } Set-Setting 'ShortcutsAsked' $true } if (-not (Read-Settings).AutostartAsked) { Write-Say '' $auto = Get-Answer 'ZECNERO_AUTOSTART' 'Start mining automatically when you sign in to Windows?' $false $false if ($auto) { Set-Autostart $true } else { Write-Say 'Mining will not start with Windows. To change that later: zecnero-miner autostart on' } Set-Setting 'AutostartAsked' $true } Write-Say '' Write-Box @('Zecnero is set up for pool mining.', '', "Commands (in $($p.Home), or from the Start menu):", ' zecnero-miner start start mining', ' zecnero-miner status hashrate, shares, huge pages', ' zecnero-miner stop stop mining', ' zecnero-miner update install the latest release (checked the same way)', '', 'Microsoft Defender may block xmrig-zecnero.exe as a miner. If it does,', 'zecnero-miner start says why and what you can do.') 'Green' if (Get-Answer 'ZECNERO_START' 'Start mining now?' $true $false) { Start-Miner $false } } function Invoke-Update([bool]$Force) { $p = Get-Paths $current = Get-InstalledVersion if (-not $current) { Stop-Setup @("the miner is not installed in $($p.Home).", "Install it with: $Script:InstallCommand") } Remove-Leftovers Initialize-Trust $latest = Get-LatestVersion if ($latest -eq $current -and -not $Force -and (Test-Installed)) { Write-Say "You have the latest version ($current)." return } $a = ConvertTo-VersionNumber $latest $b = ConvertTo-VersionNumber $current if ($null -ne $a -and $null -ne $b -and $a -lt $b -and -not $Force) { Stop-Setup @("$(Get-HostName (Get-Base)) names $latest as the latest release, older than the $current installed here.", 'Nothing was changed. To install it anyway: zecnero-miner update --force') } if ($latest -eq $current) { Write-Say "Installing $latest again." } else { Write-Say "Updating from $current to $latest." } $stage = Get-VerifiedRelease $latest $wasRunning = [bool](Get-MinerProcess) if ($wasRunning) { Stop-Miner } Install-Staged $stage Write-Say "Installed $latest. Your pool login and settings were not changed." Invoke-Installed @('migrate') if ($Script:ChildCode -ne 0) { $Script:ExitCode = $Script:ChildCode; return } if ($wasRunning) { Invoke-Installed @('start', '--minimized') if ($Script:ChildCode -ne 0) { $Script:ExitCode = $Script:ChildCode } } } # Run by update with the new zecnero-miner.ps1: brings the settings and # xmrig's config up to this version. function Invoke-Migrate { Write-Shim $s = Read-Settings if (Test-SaplingAddress "$($s.PoolLogin)") { [void](Write-XmrigConfig); Write-Say "Wrote $((Get-Paths).Xmrig) again." } else { Write-Say 'No Sapling pool login yet. Set one with: zecnero-miner pool-login new' } } function Invoke-PoolLoginCommand([string[]]$Rest) { $s = Read-Settings if ($Rest.Count -eq 0) { if ($s.PoolLogin) { Write-Say "$($s.PoolLogin) (Sapling)" } else { Write-Say 'No pool login yet. Set one with: zecnero-miner pool-login new (or pool-login ntestsapling1...)' } return } if (-not (Test-Installed)) { Stop-Setup @("the miner is not installed in $((Get-Paths).Home).", "Install it with: $Script:InstallCommand") } Set-PoolLogin $Rest[0] [void](Write-XmrigConfig) if (Get-MinerProcess) { Write-Say 'Restarting the miner with the new login.' Stop-Miner Start-Miner $true } } function Invoke-Uninstall { $p = Get-Paths if (Get-MinerProcess) { Stop-Miner } Set-Autostart $false Set-Shortcuts $false $true foreach ($d in @($p.Bin, $p.Previous, $p.Download, $p.Run, $p.Conf, $p.Logs)) { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } Remove-Item -LiteralPath $p.Shim -Force -ErrorAction SilentlyContinue Write-Say "Removed the Zecnero miner from $($p.Home)." Write-Say "Kept: your seed file ($($p.Seed)) and your wallet ($(Get-WalletFile)), if any." if ((Get-LockPagesState) -eq 'active') { Write-Say 'The "Lock pages in memory" right stays with this account (this script never changes it). To remove it: zecnero-miner huge-pages off shows the steps; run it before uninstall, or see https://zecnero.org/mine#windows-huge-pages' } } function Show-Help { Write-Say @' Zecnero Testnet pool miner for Windows (native, beta). Commands: zecnero-miner start [--minimized] start mining (xmrig in its own window) zecnero-miner stop stop mining zecnero-miner status hashrate, shares, pool, huge pages zecnero-miner update [--force] install the latest release (signature checked) zecnero-miner pool-login [new|ntestsapling1...] show or change the pool login zecnero-miner register-payout ntestsapling1... [--key-file MINING-KEY.txt] have the pool pay what it owes an nm... address to a Sapling address zecnero-miner huge-pages [on|off] huge pages: whether this account has the "Lock pages in memory" right; on/off show the steps to give or remove it yourself (needs an administrator; this script never does it) zecnero-miner autostart [on|off] start mining when you sign in to Windows zecnero-miner shortcuts [on|off] Start menu shortcuts zecnero-miner config write xmrig's config.json again zecnero-miner uninstall remove the programs (keeps your seed and wallet) This is pool mining only. Solo mining (your own node) runs in WSL: irm https://downloads.zecnero.org/install.ps1 | iex '@ } function Invoke-Main([string[]]$Arguments) { $Script:ExitCode = 0 $Script:CleanupDirs = @() $pause = $false $pauseOnError = $false $list = @() foreach ($a in $Arguments) { if ($a -eq '--pause') { $pause = $true } elseif ($a -eq '--pause-on-error') { $pauseOnError = $true } else { $list += $a } } $cmd = 'help' if ($list.Count -gt 0) { $cmd = "$($list[0])".ToLowerInvariant() } elseif (-not $Script:RunAsFile) { $cmd = 'install' } $rest = @() if ($list.Count -gt 1) { $rest = @($list[1..($list.Count - 1)]) } try { switch ($cmd) { 'install' { Invoke-Install } 'setup' { Invoke-Setup } 'update' { Invoke-Update ($rest -contains '--force') } 'migrate' { Invoke-Migrate } 'start' { Start-Miner ($rest -contains '--minimized') } 'stop' { Stop-Miner } 'status' { Show-Status } 'pool-login' { Invoke-PoolLoginCommand $rest } 'register-payout' { Invoke-RegisterPayout $rest } 'huge-pages' { $a = 'status' if ($rest.Count -gt 0) { $a = $rest[0] } switch ($a) { 'on' { Invoke-HugePages 'on' } 'off' { Invoke-HugePages 'off' } default { Invoke-HugePages 'status' } } } 'autostart' { if ($rest.Count -gt 0 -and $rest[0] -eq 'on') { Set-Autostart $true } elseif ($rest.Count -gt 0 -and $rest[0] -eq 'off') { Set-Autostart $false } elseif (Test-Path -LiteralPath (Get-AutostartLink)) { Write-Say 'Autostart is on.' } else { Write-Say 'Autostart is off.' } } 'shortcuts' { Set-Shortcuts (-not ($rest.Count -gt 0 -and $rest[0] -eq 'off')) ($rest -contains '--desktop') } 'config' { $ep = Write-XmrigConfig; Write-Say "Wrote $((Get-Paths).Xmrig) ($($ep.Algo), ports $($ep.Tls) TLS and $($ep.Plain))." } 'uninstall' { Invoke-Uninstall } 'version' { Write-Say "$(Get-InstalledVersion)" } default { Show-Help; if ($cmd -ne 'help') { $Script:ExitCode = 2 } } } } catch { if ("$($_.Exception.Message)" -eq 'ZECNERO_STOP') { Write-Host '' $lines = @($Script:StopLines) Write-Box (@("STOPPED: $($lines[0])") + @($lines | Select-Object -Skip 1)) 'Red' } else { Write-Box @('STOPPED: something unexpected went wrong.', "$($_.Exception.Message)", "Run the same command again, or ask for help on Discord.") 'Red' } $Script:ExitCode = 1 } finally { foreach ($d in $Script:CleanupDirs) { Remove-Item -LiteralPath $d -Recurse -Force -ErrorAction SilentlyContinue } } if (($pause -or ($pauseOnError -and $Script:ExitCode -ne 0)) -and (Test-Interactive)) { Read-Host 'Press Enter to close' | Out-Null } } if ($env:ZECNERO_PS_NO_MAIN -ne '1') { Invoke-Main $Script:Argv if ($Script:RunAsFile) { exit $Script:ExitCode } }