# Zecnero Testnet miner for Windows. # # Open PowerShell (no need for "Run as administrator") and run: # # irm https://downloads.zecnero.org/install.ps1 | iex # # Zecnero's miner runs inside Ubuntu on WSL, the Linux system built into # Windows. This script checks Windows, installs WSL and Ubuntu if they are # missing (that needs administrator permission and a restart), and then runs # the Linux installer inside Ubuntu: # # curl -fsSL https://downloads.zecnero.org/install-miner.sh | bash # # Where downloads.zecnero.org is slow or blocked, the same script comes from # the mirror on seed1, which is not behind Cloudflare: # # irm https://mirror.zecnero.org/install.ps1 | iex # # Either way, Ubuntu fetches the Linux installer from downloads.zecnero.org and, # if that does not answer within about 10 seconds, from mirror.zecnero.org, # together with INSTALLERS.SHA256SUMS and its signature from the same host. It # runs the installer only after gpg in Ubuntu has checked that signature with # the Zecnero release key carried in this script (fingerprint FFFC 8AC0 2BF7 # B4B4 50B4 3B1F 6532 09A3 44E7 5767) and the installer matches its checksum. # LATEST, the name of the release to install, comes with them and must match # its checksum in the same signed file; the installer is then told to install # exactly that release, and installs it only when the release's own signed # SHA256SUMS names it too (its RELEASE file). # A standard Ubuntu has gpg; where it is missing, this script offers to install # it (apt-get, as root in Ubuntu) and stops if that does not work. The Linux # installer then fetches the release files the same way, and checks their # signature the same way wherever they came from. # # The Linux installer asks whether to mine on the pool (the default, which # needs no node and starts in about a minute) or solo with your own node. # There is no Windows build of the miner: Microsoft Defender quarantines # XMRig builds for Windows, xmrig-zecnero.exe included, as # Trojan:Win64/HashvaultMiner.A. So Windows mines through WSL. # # If PowerShell says "running scripts is disabled on this system", use the # one-line command above: it does not need a script file, so that setting does # not apply. For a saved copy: powershell -ExecutionPolicy Bypass -File install.ps1 # # Settings (all optional): # $env:ZECNERO_MODE = "pool" or "solo": answer the pool or solo question # (passed on to the Linux installer) # $env:ZECNERO_POOL_LOGIN = "nutest1..." the unified address the pool pays, # or "new" to make a wallet for it. The pool pays # shielded addresses only, and from the NU2 upgrade # unified ones only; without this the Linux # installer asks. (Passed on as well.) # $env:ZECNERO_MIRROR_FIRST = "1" try mirror.zecnero.org first (passed on as well) # $env:HTTPS_PROXY, $env:ALL_PROXY, $env:NO_PROXY a proxy for the downloads. # Windows variables do not reach Ubuntu by themselves, # so these are passed on as https_proxy, all_proxy and # no_proxy, which curl in Ubuntu reads (socks5h:// works # in ALL_PROXY). Without them, the Windows proxy setting # ([System.Net.WebRequest]::DefaultWebProxy, the one # Settings > Network > Proxy sets) is passed on as # https_proxy, unless it is on this computer itself # (127.0.0.1 or localhost): Ubuntu under WSL 2 reaches # that only with networkingMode=mirrored in .wslconfig. # After a restart, the setup continues with irm, which # uses the Windows proxy setting, or -Proxy with an # http:// HTTPS_PROXY or ALL_PROXY set here. # Settings for testing (all optional): # $env:ZECNERO_DRY_RUN = "1" say what would happen, change nothing # $env:ZECNERO_PS_NO_MAIN = "1" load the functions only # $env:ZECNERO_DISTRO = "name" use this WSL distribution # $env:ZECNERO_RELEASE_BASE, $env:ZECNERO_MIRROR_BASE = "https://..." # download from these instead (passed on as well); # http:// only for the tests, with $env:ZECNERO_TEST_MODE = "1" # $env:ZECNERO_TEST_KEY_FILE = "/linux/path/key.asc", $env:ZECNERO_TEST_FINGERPRINT = "40 hex" # check the Linux installer with this key instead of the # release key (passed on as well); with ZECNERO_TEST_MODE=1 only $ErrorActionPreference = 'Stop' $Script:InstallCommand = 'irm https://downloads.zecnero.org/install.ps1 | iex' $Script:PrimaryBase = 'https://downloads.zecnero.org' $Script:MirrorBase = 'https://mirror.zecnero.org' # Where Ubuntu keeps the downloaded Linux installer until bash has opened it, # and the checked LATEST for it (export ZECNERO_EXPECT_RELEASE=testnet-vX.Y.Z). $Script:LinuxInstallerFile = '~/.zecnero-install-miner.sh' $Script:LinuxReleaseFile = '~/.zecnero-install-release' $Script:MirroredHint = 'If the download fails, add networkingMode=mirrored under [wsl2] in %UserProfile%\.wslconfig (Windows 11), run wsl --shutdown, and run this command again.' $Script:DistroToInstall = 'Ubuntu-24.04' $Script:MinBuild = 19041 $Script:RunOnceName = 'ZecneroMinerSetup' # The Zecnero release signing key, FFFC 8AC0 2BF7 B4B4 50B4 3B1F 6532 09A3 44E7 5767, # the same key install-miner.sh carries. Ubuntu checks the Linux installer with # it (see Get-VerifyScript), so a changed key on a download server cannot change # what this script trusts. Its fingerprint is checked 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----- '@ # The bash script that fetches the Linux installer, INSTALLERS.SHA256SUMS, # INSTALLERS.SHA256SUMS.asc and LATEST, all four from one host (the first, or # the second when the first does not answer), checks the signature with the # pinned key, the installer and LATEST against their checksums, and only then # saves the installer, and LATEST as ZECNERO_EXPECT_RELEASE, where the command # from Get-LinuxCommand uses them. A bad signature or checksum stops the run: it # never switches to the other host. @@...@@ are filled in by Get-VerifyScript. $Script:VerifyTemplate = @' set -u fpr='@@FPR@@' keyfile='@@KEYFILE@@' bases='@@BASES@@' out="$HOME/.zecnero-install-miner.sh" rel="$HOME/.zecnero-install-release" rm -f "$out" "$rel" stop() { printf '\nSTOPPED: %s\n' "$1" >&2 shift [ $# -gt 0 ] && printf '%s\n' "$@" >&2 exit 1 } command -v gpg >/dev/null 2>&1 || stop "gpg is needed to check the Zecnero installer, and this Ubuntu does not have it." \ "The installer runs only after a good signature from the Zecnero release key." \ "Install gpg in Ubuntu with: sudo apt update && sudo apt install -y gnupg" \ "Then run the same command again." dir="$(mktemp -d)" || stop "could not make a temporary folder." trap 'GNUPGHOME="$dir/gnupg" gpgconf --kill all >/dev/null 2>&1; rm -rf "$dir"' EXIT host() { local h="${1#*://}" printf '%s' "${h%%/*}" } fetch() { local base="$1" quick="$2" f set -- -fsSL case "$base" in https://*) set -- "$@" --proto =https --tlsv1.2 ;; esac if [ "$quick" = 1 ]; then set -- "$@" --connect-timeout 10 --max-time 60; else set -- "$@" --retry 3 --connect-timeout 20; fi rm -f "$dir/install-miner.sh" "$dir/INSTALLERS.SHA256SUMS" "$dir/INSTALLERS.SHA256SUMS.asc" "$dir/LATEST" for f in install-miner.sh INSTALLERS.SHA256SUMS INSTALLERS.SHA256SUMS.asc LATEST; do curl "$@" -o "$dir/$f" "$base/$f" || return 1 done } # shellcheck disable=SC2086 # two addresses at most, without spaces set -- $bases from="$1" if [ $# -eq 2 ] && ! fetch "$1" 1; then echo "$(host "$1") did not answer, so the installer comes from $(host "$2")." from="$2" fetch "$2" 0 || stop "could not download the installer from $(host "$2")." "Check your internet connection and run the same command again." elif [ $# -eq 1 ]; then fetch "$1" 0 || stop "could not download the installer from $(host "$1")." "Check your internet connection and run the same command again." fi h="$(host "$from")" mkdir -m 700 "$dir/gnupg" || stop "could not make a temporary folder." export GNUPGHOME="$dir/gnupg" if [ -n "$keyfile" ]; then echo "TEST MODE: the installer is checked with the key $fpr from $keyfile, not the release key." gpg --batch --quiet --no-tty --import "$keyfile" >/dev/null 2>&1 else gpg --batch --quiet --no-tty --import >/dev/null 2>&1 <<'KEY' @@KEY@@ KEY fi primary="$(gpg --batch --no-tty --with-colons --fingerprint 2>/dev/null | awk -F: '$1 == "pub" { want = 1; next } want && $1 == "fpr" { print $10; exit }')" [ "$primary" = "$fpr" ] || stop "the signing key is not the Zecnero release key." \ "Expected fingerprint $fpr, got ${primary:-nothing}. The installer was not run." status="$(gpg --batch --no-tty --status-fd 1 --verify "$dir/INSTALLERS.SHA256SUMS.asc" "$dir/INSTALLERS.SHA256SUMS" 2>/dev/null)" if printf '%s\n' "$status" | grep -Eq '^\[GNUPG:\] (BADSIG|ERRSIG|EXPSIG|EXPKEYSIG|REVKEYSIG)'; then stop "the signature of the Zecnero installer from $h is NOT valid." \ "INSTALLERS.SHA256SUMS was not signed by the Zecnero release key." \ "The installer was not run. Please report this at https://gitlab.com/zecnero or on Discord." fi printf '%s\n' "$status" | awk -v fpr="$fpr" '$1 == "[GNUPG:]" && $2 == "VALIDSIG" && $NF == fpr { ok = 1 } END { exit !ok }' || stop "the signature of the Zecnero installer from $h could not be checked." \ "gpg did not report a good signature from $fpr. The installer was not run." lines="$(grep -E '^[0-9a-f]{64} [ *]install-miner\.sh$' "$dir/INSTALLERS.SHA256SUMS")" [ "$(printf '%s' "$lines" | grep -c .)" = 1 ] || stop "INSTALLERS.SHA256SUMS from $h has no single entry for install-miner.sh." "The installer was not run." got="$(sha256sum "$dir/install-miner.sh" | cut -d' ' -f1)" [ "${lines%% *}" = "$got" ] || stop "install-miner.sh from $h does not match its signed checksum." \ "The installer was not run. Please report this at https://gitlab.com/zecnero or on Discord." lines="$(grep -E '^[0-9a-f]{64} [ *]LATEST$' "$dir/INSTALLERS.SHA256SUMS")" [ "$(printf '%s' "$lines" | grep -c .)" = 1 ] || stop "INSTALLERS.SHA256SUMS from $h has no single entry for LATEST." "The installer was not run." got="$(sha256sum "$dir/LATEST" | cut -d' ' -f1)" [ "${lines%% *}" = "$got" ] || stop "LATEST from $h does not match its signed checksum." \ "The installer was not run. A release published in the last few minutes can cause this:" \ "try again later. If it stays, please report it at https://gitlab.com/zecnero or on Discord." latest="$(cat "$dir/LATEST")" printf '%s\n' "$latest" | grep -Eqx 'testnet-v[0-9]+\.[0-9]+\.[0-9]+' || stop "the signed LATEST from $h is not a release name." "The installer was not run." echo "Signature OK: the Linux installer and LATEST ($latest) from $h are signed by the Zecnero release key $fpr." printf 'export ZECNERO_EXPECT_RELEASE=%s\n' "$latest" >"$rel" || stop "could not save the release name in $HOME." mv -f "$dir/install-miner.sh" "$out" || stop "could not save the installer in $HOME." '@ 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 } function Test-DryRun { return ($env:ZECNERO_DRY_RUN -eq '1') } 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 } } } } # Runs wsl.exe and returns its exit code and output as text. wsl.exe writes # UTF-16 unless WSL_UTF8 is set; stray NUL characters are removed either way. function Invoke-Wsl([string[]]$Arguments) { $env:WSL_UTF8 = '1' $previous = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { $out = & wsl.exe @Arguments 2>&1 | Out-String $code = $LASTEXITCODE } catch { $out = $_.Exception.Message $code = -1 } finally { $ErrorActionPreference = $previous } return [pscustomobject]@{ Code = $code; Output = ($out -replace "`0", '') } } function Get-WindowsInfo { $os = Get-CimInstance Win32_OperatingSystem $cs = Get-CimInstance Win32_ComputerSystem $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 return [pscustomobject]@{ Caption = $os.Caption Build = [int]$os.BuildNumber Is64Bit = [Environment]::Is64BitOperatingSystem Arch = $(if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }) HypervisorPresent = [bool]$cs.HypervisorPresent FirmwareVirtualization = [bool]$cpu.VirtualizationFirmwareEnabled MemoryGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1) } } # Virtualization is on if Windows already runs a hypervisor (then the CPU # reports the firmware setting as off, so that alone proves nothing), or if # the firmware setting says so. function Test-Virtualization($Info) { return ($Info.HypervisorPresent -or $Info.FirmwareVirtualization) } function Test-WslPresent { if (-not (Get-Command wsl.exe -ErrorAction SilentlyContinue)) { return $false } $status = Invoke-Wsl @('--status') return ($status.Code -eq 0) } # Lists WSL distributions as objects with Name, Version (1 or 2) and Default. function Get-WslDistros { if ($null -ne $env:ZECNERO_PS_FAKE_DISTROS) { $list = @() foreach ($entry in ($env:ZECNERO_PS_FAKE_DISTROS -split ',')) { if ($entry -match '^(.+):(\d)$') { $list += [pscustomobject]@{ Name = $Matches[1]; Version = [int]$Matches[2]; Default = $false } } } return $list } $result = Invoke-Wsl @('--list', '--verbose') if ($result.Code -ne 0) { return @() } $list = @() foreach ($line in ($result.Output -split "`r?`n")) { if ($line -match '^\s*(\*)?\s*(\S+)\s+(Running|Stopped|Installing|Converting|Uninstalling)\s+(\d)\s*$') { $list += [pscustomobject]@{ Name = $Matches[2]; Version = [int]$Matches[4]; Default = ($Matches[1] -eq '*') } } } return $list } # Picks the Ubuntu to use: the one named in ZECNERO_DISTRO, else the newest # Ubuntu, else the default distribution if it is an Ubuntu. # WSL distribution names have no spaces: "Ubuntu-24.04", not "Ubuntu 24.04" (that is the # Store's display name, sometimes with " LTS"; wsl.exe refuses it). Turns a display name into # the WSL name, so ZECNERO_DISTRO works either way. function ConvertTo-WslDistroName([string]$Name) { $n = $Name.Trim() -replace '\s+LTS$', '' return ($n -replace '\s+', '-') } function Select-UbuntuDistro($Distros) { if ($env:ZECNERO_DISTRO) { $wanted = ConvertTo-WslDistroName $env:ZECNERO_DISTRO return $Distros | Where-Object { $_.Name -eq $wanted } | Select-Object -First 1 } $ubuntus = @($Distros | Where-Object { $_.Name -match '^Ubuntu' }) if ($ubuntus.Count -eq 0) { return $null } foreach ($preferred in @('Ubuntu-24.04', 'Ubuntu', 'Ubuntu-22.04')) { $hit = $ubuntus | Where-Object { $_.Name -eq $preferred } | Select-Object -First 1 if ($hit) { return $hit } } return $ubuntus | Sort-Object Name -Descending | Select-Object -First 1 } # Explains a WSL error in plain words, from the text wsl.exe printed. function Get-WslErrorAdvice([string]$Output) { if ($Output -match '0x80370102') { return @( 'Virtualization is turned off in this computer''s firmware (BIOS/UEFI).', 'WSL 2 needs it. To turn it on:', ' 1. Restart the computer and open the BIOS/UEFI setup (usually by', ' pressing F2, F10, Del or Esc right after switching it on).', ' 2. Find "Intel Virtualization Technology" (VT-x) or "SVM Mode"', ' (AMD) and set it to Enabled. Save and exit.', ' 3. Back in Windows, run the same command again:', " $Script:InstallCommand") } if ($Output -match '0x8007019e|0x800701bc|WslRegisterDistribution failed') { return @( 'The WSL part of Windows is not fully installed yet.', 'Restart the computer, then run the same command again:', " $Script:InstallCommand") } if ($Output -match '0x80070005|access is denied|requires elevation') { return @( 'Windows did not give permission to install WSL.', 'Right-click the Start button, choose "Terminal (Admin)" or', '"Windows PowerShell (Admin)", and run the same command there:', " $Script:InstallCommand") } if ($Output -match '--install' -and $Output -match 'Usage|--list|--export') { return @( 'This Windows is too old for the one-step WSL install (wsl --install', 'printed its help instead of installing). Install the latest updates', '(Settings, Windows Update), restart, and run the same command again.') } return @('WSL reported an error:', $Output.Trim()) } # The command that continues the setup after the restart: install.ps1 from # downloads.zecnero.org, or from the mirror when that does not answer in 30 s. # irm uses the Windows proxy setting; an http:// HTTPS_PROXY or ALL_PROXY set # here (without a password, which is not stored) is given to it with -Proxy. function Get-ResumeCommand { $bases = Get-DownloadBases if ($null -eq $bases) { $bases = @($Script:PrimaryBase, $Script:MirrorBase) } $proxyArg = '' foreach ($name in @('HTTPS_PROXY', 'ALL_PROXY')) { $value = "$([Environment]::GetEnvironmentVariable($name))".Trim() if ($value -match '^https?://[^@]+$' -and (Test-PassableValue $value)) { $proxyArg = " -Proxy '$value'"; break } } $set = "`$env:ZECNERO_RESUMED='1'; " if ($env:ZECNERO_MIRROR_FIRST -eq '1') { $set += "`$env:ZECNERO_MIRROR_FIRST='1'; " } if ($bases.Count -eq 1) { return "$set" + "irm $($bases[0])/install.ps1$proxyArg | iex" } return "$set" + "try { `$s = irm $($bases[0])/install.ps1 -TimeoutSec 30$proxyArg } catch { `$s = irm $($bases[1])/install.ps1$proxyArg }; iex `$s" } function Set-ResumeAfterReboot { $command = "powershell.exe -NoExit -ExecutionPolicy Bypass -Command `"$(Get-ResumeCommand)`"" if (Test-DryRun) { Write-Say "[dry run] would add a RunOnce entry: $command"; return } New-Item -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Force | Out-Null Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\RunOnce' -Name $Script:RunOnceName -Value $command } function Install-WslUbuntu($Info) { Write-Say '' Write-Box @( 'Zecnero runs inside Ubuntu on WSL, the Linux system built into', 'Windows, and it is not installed yet. Installing it:', '', ' - asks for administrator permission (a Windows prompt: click Yes),', ' - downloads about 500 MB,', ' - needs your computer to restart.', '', 'After the restart, an Ubuntu window opens and asks you to choose a', 'username and a password. The password does not show while you', 'type. That is normal: type it and press Enter.', '', 'Then this setup continues by itself (or run the same command again).') 'Cyan' if (-not (Test-Virtualization $Info)) { Write-Say '' Write-Say 'Note: virtualization looks turned off in this computer''s firmware (BIOS/UEFI).' Write-Say 'WSL 2 needs it. If the install fails with 0x80370102, see the steps it shows.' } Write-Say '' if (-not (Test-DryRun)) { if (-not (Read-YesNo 'Install WSL and Ubuntu now?' $true)) { Write-Say "Nothing was changed. Run the command again when you are ready: $Script:InstallCommand" return } } if ($Script:DistroToInstall -match '\s') { throw "internal: the WSL distribution name has a space: '$Script:DistroToInstall'" } if (Test-DryRun) { Write-Say "[dry run] would run: wsl.exe --update" Write-Say "[dry run] would run: wsl.exe --install -d $Script:DistroToInstall" $result = [pscustomobject]@{ Code = 0; Output = '' } } else { # A current WSL first: an old one fails some installs with confusing errors. Not fatal: # without WSL on an older Windows this prints its help, and the install below handles that. Write-Say 'Updating WSL first (wsl --update)...' $update = Invoke-Wsl @('--update') if ($update.Code -ne 0 -or ($update.Output -match 'Usage:' -and $update.Output -match '--exec')) { Write-Say 'wsl --update did not run here; continuing with the install.' } Write-Say "Installing WSL and $Script:DistroToInstall. This takes a few minutes, and Windows asks for permission..." $result = Invoke-Wsl @('--install', '-d', $Script:DistroToInstall) Write-Say $result.Output.Trim() # An old wsl.exe prints its help for --install, sometimes with exit code 0. if ($result.Output -match 'Usage:' -and $result.Output -match '--exec') { $result = [pscustomobject]@{ Code = 1; Output = $result.Output } } } if ($result.Code -ne 0) { Write-Say '' Write-Box (@('STOPPED: WSL could not be installed.', '') + (Get-WslErrorAdvice $result.Output)) 'Red' return } Set-ResumeAfterReboot Write-Say '' Write-Box @( 'WSL and Ubuntu are installed. Your computer needs to restart.', '', 'After the restart:', ' 1. An Ubuntu window asks for a new username and password.', ' The password does not show while you type. That is normal.', ' 2. This setup opens again by itself and finishes the install.', ' If it does not, open PowerShell and run the same command again:', " $Script:InstallCommand") 'Green' if (-not (Test-DryRun)) { if (Read-YesNo 'Restart the computer now?' $false) { Restart-Computer } else { Write-Say 'Restart when you are ready. Save your work first.' } } } # 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 ($env:ZECNERO_TEST_MODE -eq '1' -and $base -cmatch '^http://(127\.0\.0\.1|[a-z0-9-]+\.zecnero\.test):\d+(/[A-Za-z0-9._~/-]*)?$') { continue } return $null } return , $bases } # The key the Linux installer is checked with: the release key, or in test mode # ($env:ZECNERO_TEST_MODE = "1") a test key file inside Ubuntu with its # fingerprint. $null for test values this script cannot pass on safely. function Get-TrustSettings { $trust = [pscustomobject]@{ Fingerprint = $Script:ReleaseFingerprint; KeyFile = '' } if ($env:ZECNERO_TEST_MODE -ne '1') { return $trust } $fpr = "$env:ZECNERO_TEST_FINGERPRINT".Trim().ToUpperInvariant() $file = "$env:ZECNERO_TEST_KEY_FILE".Trim() if ($fpr -eq '' -and $file -eq '') { return $trust } if ($fpr -notmatch '^[0-9A-F]{40}$' -or $file -notmatch '^/' -or -not (Test-PassableValue $file)) { return $null } return [pscustomobject]@{ Fingerprint = $fpr; KeyFile = $file } } # The bash script from $Script:VerifyTemplate for these download bases and key. function Get-VerifyScript($Bases, $Trust) { $script = $Script:VerifyTemplate.Replace('@@FPR@@', $Trust.Fingerprint).Replace('@@KEYFILE@@', $Trust.KeyFile) $script = $script.Replace('@@BASES@@', ($Bases -join ' ')).Replace('@@KEY@@', $Script:ReleaseKey.Trim()) return ($script -replace "`r", '') } # The Linux installer runs only after gpg in Ubuntu has checked its signature. function Get-GpgAdvice([string]$Distro) { return @( 'STOPPED: gpg is needed to check the Zecnero installer,', "and $Distro does not have it. The installer runs only", 'after a good signature from the Zecnero release key.', "Install gpg: open $Distro and run", ' sudo apt update && sudo apt install -y gnupg', 'or run this in PowerShell:', " wsl -d $Distro -u root -- sh -c 'apt-get update && apt-get install -y gnupg'", 'Then run the same command again.') } # A standard Ubuntu has gpg. Where it is missing, this offers to install it as # root inside Ubuntu (wsl -u root needs no password). $true once gpg is there. function Install-UbuntuGpg([string]$Distro) { $apt = 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y gnupg' Write-Say '' Write-Say "$Distro has no gpg, which checks the signature of the Zecnero installer before it runs." if (Test-DryRun) { Write-Say "[dry run] would ask, then run: wsl.exe -d $Distro -u root -- sh -c '$apt'"; return $true } if (-not (Read-YesNo "Install gpg in $Distro now (apt-get, as root)?" $true)) { return $false } & wsl.exe -d $Distro -u root -- sh -c $apt | Out-Host $check = Invoke-Wsl @('-d', $Distro, '--', 'sh', '-c', 'command -v gpg >/dev/null && echo has-gpg') return ($check.Output -match 'has-gpg') } # A value this script can hand to bash inside single quotes, in a command line # that wsl.exe passes through a shell: no quotes, spaces, $ or backticks. function Test-PassableValue([string]$Value) { return ($Value -cmatch '^[A-Za-z0-9._~:/@%+=,*\[\]-]+$') } function Test-LoopbackProxy([string]$Proxy) { return ($Proxy -match '^([a-z0-9]+://)?([^/@]*@)?(127\.[0-9.]+|localhost|\[::1\])(:|/|$)') } function Test-WslMirroredNetworking { if ($null -ne $env:ZECNERO_PS_FAKE_WSL_NETWORKING) { return ($env:ZECNERO_PS_FAKE_WSL_NETWORKING -eq 'mirrored') } $config = Join-Path $env:USERPROFILE '.wslconfig' if (-not (Test-Path $config)) { return $false } return ((Get-Content -Raw $config) -match '(?im)^\s*networkingMode\s*=\s*mirrored\s*$') } # The proxy Windows itself uses for the download server (Settings > Network > # Proxy, or a PAC script), as http://host:port, or $null for none. function Get-SystemProxy { if ($null -ne $env:ZECNERO_PS_FAKE_SYSTEM_PROXY) { if ($env:ZECNERO_PS_FAKE_SYSTEM_PROXY -eq '') { return $null } return $env:ZECNERO_PS_FAKE_SYSTEM_PROXY } try { $target = [uri]"$Script:PrimaryBase/install-miner.sh" $proxy = [System.Net.WebRequest]::DefaultWebProxy if ($null -eq $proxy) { return $null } $via = $proxy.GetProxy($target) if ($null -eq $via -or $via.Authority -eq $target.Authority) { return $null } return "http://$($via.Authority)" } catch { return $null } } function Hide-Password([string]$Proxy) { return ($Proxy -replace '^([a-z0-9]+://)?[^/@]*@', '$1***@') } # The proxy settings to hand to Ubuntu, as name=value pairs for curl, and the # lines to say about them. Windows variable names ignore case, so HTTPS_PROXY # and https_proxy are one variable here. function Get-ProxyForLinux { $vars = [ordered]@{} $notes = @() $mirrored = Test-WslMirroredNetworking foreach ($name in @('HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY')) { $value = "$([Environment]::GetEnvironmentVariable($name))".Trim() if ($value -eq '') { continue } if (-not (Test-PassableValue $value)) { $notes += "Not passing $name on to Ubuntu: it has characters this setup cannot pass safely. Set it inside Ubuntu instead." continue } if ($name -ne 'NO_PROXY' -and (Test-LoopbackProxy $value) -and -not $mirrored) { $notes += "Not passing $name ($(Hide-Password $value)) on to Ubuntu: WSL 2 cannot reach a proxy on this computer. $Script:MirroredHint" continue } $vars[$name.ToLowerInvariant()] = $value $notes += "Passing $name on to Ubuntu: $(Hide-Password $value)" } if (-not $vars.Contains('https_proxy') -and -not $vars.Contains('all_proxy')) { $system = Get-SystemProxy if ($system) { if (-not (Test-PassableValue $system)) { $notes += 'Windows uses a proxy this setup cannot pass on to Ubuntu. Set https_proxy inside Ubuntu if the download fails.' } elseif ((Test-LoopbackProxy $system) -and -not $mirrored) { $notes += "Not passing the Windows proxy $system on to Ubuntu: WSL 2 cannot reach a proxy on this computer. $Script:MirroredHint" } else { $vars['https_proxy'] = $system $notes += "Passing the Windows proxy setting on to Ubuntu: https_proxy=$system" } } } return [pscustomobject]@{ Vars = $vars; Notes = $notes } } # The command run inside Ubuntu. Its first part is the bash script from # Get-VerifyScript, handed over in base64: it fetches the Linux installer with # INSTALLERS.SHA256SUMS, its signature and LATEST from the first host, or from # the second when the first does not answer, checks them, and saves the # installer and the checked LATEST only when the check passes. Then the # installer runs with the settings from here (Windows variables do not reach # WSL by themselves) and ZECNERO_EXPECT_RELEASE from that file. wsl.exe hands # this line to a shell inside double quotes, so it must not contain $ or double # quotes: the files are removed once bash has them open. function Get-LinuxCommand { $mode = "$env:ZECNERO_MODE".Trim().ToLowerInvariant() $login = "$env:ZECNERO_POOL_LOGIN".Trim() if ($mode -ne '' -and $mode -ne 'pool' -and $mode -ne 'solo') { return $null } if ($login -ne '' -and -not (Test-PoolLogin $login)) { return $null } $bases = Get-DownloadBases if ($null -eq $bases) { return $null } $trust = Get-TrustSettings if ($null -eq $trust) { return $null } $vars = @() if ($mode -ne '') { $vars += "ZECNERO_MODE=$mode" } if ($login -ne '') { $vars += "ZECNERO_POOL_LOGIN=$login" } if ($env:ZECNERO_MIRROR_FIRST -eq '1') { $vars += 'ZECNERO_MIRROR_FIRST=1' } $release = "$env:ZECNERO_RELEASE_BASE".Trim().TrimEnd('/') $mirror = "$env:ZECNERO_MIRROR_BASE".Trim().TrimEnd('/') if ($release -ne '') { $vars += "ZECNERO_RELEASE_BASE=$release" } if ($mirror -ne '') { $vars += "ZECNERO_MIRROR_BASE=$mirror" } if ($env:ZECNERO_TEST_MODE -eq '1') { $vars += 'ZECNERO_TEST_MODE=1' } if ($trust.KeyFile -ne '') { $vars += @("ZECNERO_TEST_KEY_FILE=$($trust.KeyFile)", "ZECNERO_TEST_FINGERPRINT=$($trust.Fingerprint)") } $proxy = (Get-ProxyForLinux).Vars $command = '' if ($proxy.Count -gt 0) { $command = 'export ' + (($proxy.Keys | ForEach-Object { "$_='$($proxy[$_])'" }) -join ' ') + '; ' } $check = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes((Get-VerifyScript $bases $trust))) $command += "echo $check | base64 -d | bash -s" $run = (@($vars) + 'bash') -join ' ' $command += " && . $Script:LinuxReleaseFile && { rm -f $Script:LinuxInstallerFile $Script:LinuxReleaseFile && $run; } < $Script:LinuxInstallerFile" return $command } # The pool pays shielded addresses only: its login is a Testnet unified # address (nutest1 and at least 104 bech32 characters), a Sapling one until # NU2 (ntestsapling1 and 75; the Linux installer refuses it from NU2), or # "new" for a wallet the Linux installer makes. An nm... address is refused. function Test-PoolLogin([string]$Login) { if ($Login -ceq 'new') { return $true } return ($Login -cmatch '^(nutest1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{104,}|ntestsapling1[qpzry9x8gf2tvdw0s3jn54khce6mua7l]{75})$') } function Invoke-LinuxInstaller($Distro) { $linuxCommand = Get-LinuxCommand if ($null -eq $linuxCommand -and $null -eq (Get-DownloadBases)) { Write-Box @('STOPPED: 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') 'Red' return } if ($null -eq $linuxCommand) { if ("$env:ZECNERO_POOL_LOGIN".Trim() -ne '' -and -not (Test-PoolLogin "$env:ZECNERO_POOL_LOGIN".Trim())) { Write-Box @("STOPPED: ZECNERO_POOL_LOGIN is `"$env:ZECNERO_POOL_LOGIN`".", 'The pool pays shielded addresses only, so it needs a unified', 'address (nutest1...), not an nm... address.', 'Set it to your unified address, or to new to make a wallet,', 'or remove it to be asked: Remove-Item Env:ZECNERO_POOL_LOGIN') 'Red' return } if ($null -eq (Get-TrustSettings)) { Write-Box @('STOPPED: ZECNERO_TEST_FINGERPRINT or ZECNERO_TEST_KEY_FILE cannot be used.', 'In test mode they need a 40-character fingerprint and a Linux path', '(starting with /) inside Ubuntu, both set. Remove them to use the release key:', 'Remove-Item Env:ZECNERO_TEST_FINGERPRINT, Env:ZECNERO_TEST_KEY_FILE') 'Red' return } Write-Box @("STOPPED: ZECNERO_MODE is `"$env:ZECNERO_MODE`".", 'Set it to pool or solo, or remove it to be asked:', 'Remove-Item Env:ZECNERO_MODE') 'Red' return } Write-Say "Using $($Distro.Name) (WSL $($Distro.Version))." if ($env:ZECNERO_TEST_MODE -ne '1' -and "$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.' } foreach ($note in (Get-ProxyForLinux).Notes) { Write-Say $note } if ($Distro.Version -ne 2) { Write-Box @( "$($Distro.Name) runs on WSL 1. Zecnero needs WSL 2.", 'Converting it takes a few minutes and keeps your files.') 'Yellow' if (Test-DryRun) { Write-Say "[dry run] would run: wsl.exe --set-version $($Distro.Name) 2" } elseif (Read-YesNo "Convert $($Distro.Name) to WSL 2 now?" $true) { & wsl.exe --set-version $Distro.Name 2 if ($LASTEXITCODE -ne 0) { $probe = Invoke-Wsl @('--set-version', $Distro.Name, '2') Write-Box (@('STOPPED: the conversion to WSL 2 failed.', '') + (Get-WslErrorAdvice $probe.Output)) 'Red' return } } else { Write-Say "Nothing was changed. To convert it yourself: wsl --set-version $($Distro.Name) 2" return } } if (Test-DryRun) { Write-Say "[dry run] would check that $($Distro.Name) starts and has a user, curl and gpg" Write-Say "[dry run] Ubuntu would run install-miner.sh from $((Get-DownloadBases) -join ' or ') only once INSTALLERS.SHA256SUMS from the same host has a good signature from $((Get-TrustSettings).Fingerprint) and lists it and LATEST, and install exactly that release" Write-Say "[dry run] would run: wsl.exe -d $($Distro.Name) -- bash -c `"$($linuxCommand -replace '(://)[^/@'' ]*@', '$1***@')`"" return } # Start it once with the output captured, to catch start-up errors # (0x80370102 and friends) and a Ubuntu nobody has set up yet. $probe = Invoke-Wsl @('-d', $Distro.Name, '--', 'sh', '-c', 'id -u; command -v curl >/dev/null && echo has-curl; command -v gpg >/dev/null && echo has-gpg') if ($probe.Code -ne 0) { Write-Box (@("STOPPED: $($Distro.Name) did not start.", '') + (Get-WslErrorAdvice $probe.Output)) 'Red' return } $lines = @($probe.Output -split "`r?`n" | Where-Object { $_ -ne '' }) if ($lines.Count -gt 0 -and $lines[0].Trim() -eq '0') { Write-Box @( "$($Distro.Name) is installed but not set up yet.", "Open `"$($Distro.Name)`" from the Start menu. It asks for a new username", 'and password (the password does not show while you type; that is', 'normal). Then close it and run the same command again:', "$Script:InstallCommand") 'Yellow' return } if ($probe.Output -notmatch 'has-curl') { Write-Box @( "$($Distro.Name) has no curl, which the installer needs.", "Open $($Distro.Name) and run this line, then run the same command again:", 'sudo apt update && sudo apt install -y curl') 'Yellow' return } if ($probe.Output -notmatch 'has-gpg' -and -not (Install-UbuntuGpg $Distro.Name)) { Write-Box (Get-GpgAdvice $Distro.Name) 'Red' return } Write-Say '' Write-Say "Running the Zecnero installer inside $($Distro.Name)..." Write-Say '' # Run directly, not captured: Ubuntu then gets this console as its # terminal, which the installer needs to show your key and ask for SAVED. & wsl.exe -d $Distro.Name -- bash -c $linuxCommand $code = $LASTEXITCODE Write-Say '' if ($code -eq 0) { Write-Say "Done. To check on the miner later, open $($Distro.Name) and run: zecnero-miner status" Write-Say 'To switch between pool and solo mining later: zecnero-miner mode pool, or zecnero-miner mode solo' Write-Say 'The pool pays your unified address; to see or change it: zecnero-miner pool-login' } else { Write-Say "The installer stopped (exit code $code). Read the message above, then run the same command again:" Write-Say $Script:InstallCommand } } function Invoke-Main { Write-Say 'Zecnero Testnet miner setup for Windows' Write-Say '' $info = Get-WindowsInfo Write-Say "Windows: $($info.Caption), build $($info.Build), $($info.Arch), $($info.MemoryGB) GB memory" if (-not $info.Is64Bit -or $info.Arch -eq 'ARM64' -or $info.Arch -eq 'x86') { Write-Box @( 'STOPPED: this processor is not supported.', 'Zecnero''s programs are built for 64-bit Intel and AMD processors.', "This computer reports $($info.Arch).") 'Red' return } if ($info.Build -lt $Script:MinBuild) { Write-Box @( "STOPPED: this Windows is too old (build $($info.Build)).", "WSL 2 needs Windows 10 build $Script:MinBuild or newer, or Windows 11.", 'Install the latest updates (Settings, Windows Update), restart,', 'and run the same command again.') 'Red' return } if ($env:ZECNERO_RESUMED -eq '1') { Write-Say '' Write-Say 'Welcome back. If an Ubuntu window is asking for a username and password,' Write-Say 'finish that first (the password does not show while you type).' if (-not (Test-DryRun)) { Read-Host 'Then press Enter here to continue' | Out-Null } } $hasWsl = Test-WslPresent $distros = @() if ($hasWsl) { $distros = @(Get-WslDistros) } $ubuntu = Select-UbuntuDistro $distros if (-not $hasWsl -or -not $ubuntu) { if ($hasWsl) { Write-Say 'WSL is installed, but there is no Ubuntu in it yet.' } else { Write-Say 'WSL is not installed yet.' } Install-WslUbuntu $info return } Invoke-LinuxInstaller $ubuntu } if ($env:ZECNERO_PS_NO_MAIN -ne '1') { try { Invoke-Main } catch { Write-Box @('STOPPED: something unexpected went wrong.', $_.Exception.Message, "Run the same command again, or ask for help on Discord: $Script:InstallCommand") 'Red' } }