diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e068ad2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,8 @@ +# Published installers and their checksum files must be byte-identical on +# every checkout. A Windows clone with core.autocrlf=true would otherwise turn +# LF into CRLF, break the published SHA-256, and fail the byte-exact tests. +static/scripts/*.sh text eol=lf +static/scripts/*.ps1 text eol=lf +static/scripts/*.sha256 text eol=lf +static/mining-compatibility.json text eol=lf +static/scripts/*.cmd text eol=crlf diff --git a/bun.lock b/bun.lock index 83f8645..4d75164 100644 --- a/bun.lock +++ b/bun.lock @@ -22,6 +22,7 @@ "@docusaurus/module-type-aliases": "^3.10.2", "@docusaurus/tsconfig": "^3.10.2", "@docusaurus/types": "^3.10.2", + "@types/bun": "^1.4.0", "typescript": "~5.6.2", }, }, @@ -742,6 +743,8 @@ "@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="], + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], "@types/connect-history-api-fallback": ["@types/connect-history-api-fallback@1.5.4", "", { "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw=="], @@ -1016,6 +1019,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], "bytes": ["bytes@3.0.0", "", {}, "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw=="], diff --git a/docs/guides/miner-app.md b/docs/guides/miner-app.md index 593c330..f469b81 100644 --- a/docs/guides/miner-app.md +++ b/docs/guides/miner-app.md @@ -1,55 +1,71 @@ --- sidebar_position: 2 -title: Miner App (GUI) -draft: true +title: Miner App Preview --- -# Miner App +# Miner App Preview -The Quantus Miner App is the easiest way to start mining. It provides a graphical interface for both CPU and GPU mining -- no terminal required. +The Quantus Miner App provides one desktop interface for wallet setup, node sync, CPU or GPU mining, hash rate, and rewards. Release `miner-v0.6.1` is a **preview**, not the verified beginner path. -## Download +The app downloads and starts node and miner binaries at runtime. It does not bundle them, pin the shared compatibility manifest, or verify their release checksums in this release. This release was built for the retired Planck testnet and has not been verified against mainnet. Do not use it for mainnet funds; use the verified installers in [Start Mining](/guides/mining). -Download the latest Miner App for your platform from [GitHub Releases](https://github.com/Quantus-Network/quantus-apps/releases/latest): +## Direct downloads -| Platform | File | -|----------|------| -| macOS (Intel / Apple Silicon) | `quantus_miner_macos.zip` | -| Linux (x64) | `quantus_miner_linux.tar.gz` | -| Windows (x64) | `quantus_miner_windows.zip` | +| Platform | Download | SHA-256 | +| --- | --- | --- | +| Windows x64 | [quantus_miner_windows.zip](https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_windows.zip) | `081308152ba6f9eba62b9ac143f8c9e115ffab400a978a9d03a996e55ece8ad1` | +| macOS | [quantus_miner_macos.zip](https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_macos.zip) | `fb4769d4c1e8ccd3c480e0caeca07c9baa2b3ef0540828894dddeea3d4ed016e` | +| Linux x64 | [quantus_miner_linux.tar.gz](https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_linux.tar.gz) | `fd2755f2fe5df05af3deb8e1081fa808f6c872bd9d4bf2853eea0fe354655076` | -## Prerequisites +These URLs and hashes are also recorded in the [compatibility manifest](/mining-compatibility.json). -Before you start mining, you need: +Verify the downloaded archive before opening it. -1. A Quantus wallet address to receive rewards. Download the Quantus mobile wallet from [quantus.com/wallet](https://www.quantus.com/wallet/) and create an account. +### Windows PowerShell -## Installation +```powershell +(Get-FileHash .\quantus_miner_windows.zip -Algorithm SHA256).Hash.ToLower() +# Expected: 081308152ba6f9eba62b9ac143f8c9e115ffab400a978a9d03a996e55ece8ad1 +``` -**macOS:** Download the `.zip` file, extract it into your desired directory, and run `Quantus Miner.app`. If macOS blocks the app, go to System Settings > Privacy & Security and click "Open Anyway." +### macOS Terminal -**Linux:** Download the `.tar.gz` file, extract it into your desired directory, and run the `quantus_miner` executable. +```bash +shasum -a 256 quantus_miner_macos.zip +# Expected: fb4769d4c1e8ccd3c480e0caeca07c9baa2b3ef0540828894dddeea3d4ed016e +``` -## Note on Syncing -Once you input your inner hash (or seed phrase) you can begin syncing your node. +### Linux Terminal -Wait until the node is fully synced, and then begin mining. +```bash +sha256sum quantus_miner_linux.tar.gz +# Expected: fd2755f2fe5df05af3deb8e1081fa808f6c872bd9d4bf2853eea0fe354655076 +``` -Depending on your internet speed it may take 5-30 minutes for the node to full sync. +## Install -## Monitoring +### Windows -Check your mining progress on the [telemetry dashboard](https://telemetry.quantus.cat/). +1. Download and extract `quantus_miner_windows.zip`. +2. Open the extracted folder and run `quantus_miner.exe`. +3. If Windows Defender warns, verify the SHA-256 above before choosing to run it. -## Troubleshooting +### macOS -| Problem | Solution | -|---------|----------| -| macOS blocks the app | System Settings > Privacy & Security > "Open Anyway" | -| App won't connect | Make sure your Quantus node is running first | -| Low hash rate | Increase CPU workers or enable GPU mining in settings | -| Mining not producing blocks | Verify your node is fully synced before mining | +1. Download and extract `quantus_miner_macos.zip`. +2. Open `Quantus Miner.app`. +3. If macOS blocks it, open System Settings, Privacy & Security, then choose Open Anyway. -## Next Steps +### Linux x64 -For advanced mining configuration (external miner, build from source), see the full [Mining and Running a Node](./mining) guide. +1. Download and extract `quantus_miner_linux.tar.gz`. +2. Mark `quantus_miner` executable with `chmod u+x quantus_miner`. +3. Run `./quantus_miner`. + +The release does not publish minimum OS versions. The release team must supply those facts before the app can be presented as universally supported. + +## What the app handles + +The app downloads the node and external miner, creates the local node identity, starts both processes, monitors sync, and shows mining statistics. A recovery phrase entered into the app is handled locally by its wallet flow. + +The app is ready to become the primary route after it consumes the shared compatibility manifest, verifies every runtime download, and avoids exposing reward material in logs or process arguments. Until then, use the [verified terminal guide](/guides/mining) where supported. diff --git a/docs/guides/mining.md b/docs/guides/mining.md index ea29935..772bad6 100644 --- a/docs/guides/mining.md +++ b/docs/guides/mining.md @@ -1,299 +1,145 @@ --- sidebar_position: 1 -title: Mining and Running a Node +title: Start Mining --- -# Mining and Running a Node +# Start Mining -This guide covers connecting to Quantus **mainnet** and mining. Works on macOS and Linux (including WSL2 on Windows). +This guide connects a computer to **Quantus mainnet** and starts the supported node and miner pair. Mainnet QTC has value, so treat your recovery phrase like the keys to a bank account. -Use `--chain mainnet` (not `planck` — that is the retired public testnet). You need node **v1.0.1 or newer**; older binaries do not include the mainnet chain spec. +## Set up with an agent -Use the **Copy Context** button at the top of this page to copy everything as Markdown -- the full guide plus an AI mining skill. Paste it to an agent like Claude Code to be walked through setup interactively, or keep it as an offline reference. +Paste this into your coding agent: -## Migrating from Planck - -Planck is a different chain. Mainnet does not share its history, database, or rewards. - -1. Install a node **v1.0.1+** from [Releases](https://github.com/Quantus-Network/chain/releases/latest). -2. Change `--chain planck` to `--chain mainnet`, or `./quantus-mining.sh config set CHAIN mainnet`. -3. Sync from genesis into `.../chains/mainnet/`. Do not copy or reuse `.../chains/planck/`. -4. Keep the same wormhole inner hash if you want rewards at the same address. The installer `--force` path does this automatically — do not generate a new keypair. - -Do not pass `--force-authoring` to join mainnet. That flag is only for bootstrapping a brand-new network. - -## Prerequisites - -Before starting, you will need: - -1. **Quantus wallet.** Download the [Quantus wallet](https://linktr.ee/quantusnetwork) to hold funds, send transactions, and spend your mining rewards. - -2. **A mnemonic / seed phrase.** You can create one in the wallet app or in the CLI. - -3. **A wormhole address for rewards.** The chain only accumulates mining rewards to **wormhole addresses** -- not regular wallet addresses. The wallet app supports wormhole addresses and encrypted accounts, so rewards mined to your wormhole address show up in the app and can be spent directly from your wormhole balance -- no separate claiming step. You'll generate your wormhole address in the setup below. - -## Understanding Wormhole Addresses - -Mining rewards are sent to a **wormhole address** derived from a 32-byte preimage you generate during setup (aka your inner hash). - -This is privacy-preserving by default. All mining rewards are paid to wormhole addresses. - -Wormhole addresses look identical to regular transparent addresses, but they have a separate derivation path. - -**If you already have an existing wallet:** You can derive a wormhole keypair from an existing mnemonic or seed instead of generating a fresh one. This is the recommended approach if you are mining for the first time -- use the same seed phrase as your wallet app, and your mining rewards will appear in the app automatically, spendable straight from your wormhole balance. - -During setup you will run `key quantus --scheme wormhole`, which outputs three values: - -| Value | What it is | What to do | -|-------|-----------|------------| -| **Address** | Your wormhole address (where rewards are sent) | Note for monitoring | -| **Inner Hash** | 32-byte preimage | Pass to the node via `--rewards-inner-hash` | -| **Secret phrase** | The mnemonic that proves ownership | Back up securely -- this recovers everything | - -The node derives your wormhole address from the `inner_hash` and logs it on startup. - -The most important thing to back up is your 24 word phrase. - -You should keep both your 24 word phrase and your secret secure and do not share either with anyone. - ---- - -## Automated Setup - -For a guided terminal workflow, use the mining setup script (macOS, Linux, or WSL2): - -```bash -curl -fsSL https://docs.quantus.com/scripts/quantus-mining.sh -o quantus-mining.sh -chmod +x quantus-mining.sh -./quantus-mining.sh setup -./quantus-mining.sh start +```text +Fetch and follow https://docs.quantus.com/agent-setup/prompt.md to set up Quantus mining on this computer. ``` -The script generates your wormhole inner hash, node identity, and a config file at `~/quantus-mining/mining.conf` (`CHAIN=mainnet`). It downloads native `quantus-node` and `quantus-miner` binaries into `~/quantus-mining/bin/`. GPU mining is recommended; the miner runs on the host so it can use Metal / Vulkan / DirectX. - -If you already have a Planck-era `mining.conf`, switch with `./quantus-mining.sh config set CHAIN mainnet`, then `./quantus-mining.sh setup --force` to download a v1.0.1+ node. `--force` refreshes binaries only — it keeps your existing `INNER_HASH` and wormhole address. Confirm with `./quantus-mining.sh config show` (`CHAIN=mainnet`, same masked inner hash). Planck chain data is not reused. - -The script checks the downloaded pair's `--help` output and only passes auth/TLS flags when **both** binaries support miner QUIC auth (`quantus-miner/2`). Mixing an auth-capable node with a pre-auth miner (or the reverse) is rejected. Pin a matching pair with `NODE_VERSION` / `MINER_VERSION` in `mining.conf` (or `./quantus-mining.sh config set NODE_VERSION `), then run `./quantus-mining.sh setup --force` to download those tags. Environment variables of the same name override the file. Unset pins fetch GitHub `releases/latest` independently and may not match. - -**Linux ARM64:** there is no native `quantus-miner` release. Mine from macOS or Linux x86_64. - -If an older installer created a Docker stack under `~/quantus-mining/docker/`, `./quantus-mining.sh stop` and `uninstall` still shut it down. New Docker setup is not supported (`setup --mode docker` is rejected). After stopping, migrate with `./quantus-mining.sh setup --force`. - -### Running the stack - -**One terminal:** `./quantus-mining.sh start` +The agent handles setup and checks. Enter wallet recovery words only in your private local terminal, never in chat. Prefer manual setup? Continue below. -Node output in your terminal (foreground); miner runs in the background (`~/quantus-mining/logs/miner.log`). Ctrl+C stops both. +## Before you start -**Two terminals (matches the manual steps below):** run `./quantus-mining.sh start-node` in one terminal, then `./quantus-mining.sh start-miner` in another after the node is listening. +Have **100 GB free on the node data drive**, as specified by the [pinned node guide](https://github.com/Quantus-Network/chain/blob/v1.0.1/MINING.md#system-requirements). Windows setup checks this before downloads or wallet entry. `preflight` checks Windows platform and storage without starting anything. To use another drive, set `QUANTUS_NODE_DATA_PATH` to your chosen data folder in the same shell before setup and future starts. -Add `-d` or `--detach` to run both in the background. Stop with `./quantus-mining.sh stop` from any terminal (works for foreground and detached runs). +You need a Quantus wallet with its 24-word recovery phrase. If you do not have one, install the Quantus Wallet app from [Tools and Community](/reference/tools-and-community) and create a wallet there: mining rewards to that phrase then appear in the app and are spendable directly. Write the phrase on paper and keep it offline. Never paste it into chat, email, a support ticket, or a command. -Manage settings with `./quantus-mining.sh config show` or `./quantus-mining.sh config set CPU_WORKERS 4`. Editable keys: `NODE_NAME`, `CPU_WORKERS`, `GPU_DEVICES`, `MINER_LISTEN_PORT`, `CHAIN`, `NODE_VERSION`, `MINER_VERSION`. Changing version pins does not swap binaries until you re-run `setup --force`. +There are two verified installers, one per shell, and they behave identically: `quantus-mining.sh` for macOS, Linux x64, and WSL2, and `quantus-mining.ps1` for native Windows 10/11 x64. Both read the same [compatibility manifest](/mining-compatibility.json), verify the same checksums, ask for the recovery phrase the same way, and print the same status. On Windows, use PowerShell rather than WSL2: the miner needs the native graphics driver to use the GPU. The [desktop Miner App](/guides/miner-app) remains a preview and is not the verified path. -GPU mining is recommended when available. Mining rewards accumulate at your wormhole address and appear in the wallet app, ready to spend. +## Three steps -The script wires miner authentication when the downloaded pair supports it: after the node starts it reads `miner-auth-token` and `miner-tls-cert-sha256` from the node's chain directory and passes them to the miner. You do not need to copy those values by hand. If the pair predates miner auth, the script starts without those flags. +### 1. Open a shell -Example config template: [mining.conf.example](/scripts/mining.conf.example). +On macOS or Linux, open Terminal. On Windows, press the Windows key, type `PowerShell`, and press Enter. No WSL is needed. -## Manual Installation (Mac / Linux) +### 2. Run the verified installer -### 1. Download the Node Binary +This downloads the script to disk, verifies its SHA-256 checksum, and only then runs it. It does not pipe remote code into a shell. -Get a `quantus-node` binary that matches your miner from [GitHub Releases](https://github.com/Quantus-Network/chain/releases). Do not mix `chain/releases/latest` with an unrelated `quantus-miner` latest tag — they are published independently and may not speak the same miner protocol. - -Download it in your working directory. - -Extract it into your working directory (on macOS, double-clicking the archive works). - -Note: `aarch64-apple` builds are for Apple Silicon Macs (M1 and above); `x86-apple` is for Intel-based Macs. - -Now open your terminal to generate your node key and inner hash, and run the node in this terminal window. - -**macOS only -- fix Gatekeeper permissions:** +macOS, Linux, or WSL2: ```bash -xattr -d com.apple.quarantine quantus-node -chmod u+x quantus-node +curl --proto '=https' --tlsv1.2 -fsS https://docs.quantus.com/scripts/quantus-mining.sh -o quantus-mining.sh && \ +curl --proto '=https' --tlsv1.2 -fsS https://docs.quantus.com/scripts/quantus-mining.sh.sha256 -o quantus-mining.sh.sha256 && \ +{ if command -v sha256sum >/dev/null 2>&1; then sha256sum -c quantus-mining.sh.sha256; else shasum -a 256 -c quantus-mining.sh.sha256; fi; } && \ +chmod u+x quantus-mining.sh && ./quantus-mining.sh mine ``` -### 2. Generate Node Identity +Windows PowerShell: -```bash -./quantus-node key generate-node-key --file node_key.p2p +```powershell +[Net.ServicePointManager]::SecurityProtocol = 'Tls12' +Invoke-WebRequest -UseBasicParsing https://docs.quantus.com/scripts/quantus-mining.ps1 -OutFile quantus-mining.ps1 +Invoke-WebRequest -UseBasicParsing https://docs.quantus.com/scripts/quantus-mining.ps1.sha256 -OutFile quantus-mining.ps1.sha256 +if ((Get-FileHash quantus-mining.ps1 -Algorithm SHA256).Hash.ToLower() -ne (Get-Content quantus-mining.ps1.sha256).Split(' ')[0]) { throw 'Checksum mismatch. Delete both files and retry.' } +Unblock-File quantus-mining.ps1 +powershell -ExecutionPolicy Bypass -File .\quantus-mining.ps1 mine ``` -### 3. Generate Inner Hash -Have the 24 word secret phrase from your wallet app ready. +The installer selects the published pair from the [compatibility manifest](/mining-compatibility.json): node `v1.0.1`, miner `v4.2.0`, and protocol `quantus-miner/2`. It verifies every release asset before installation. There are no version or network choices. -**Note: Keep this secret phrase secure and do not share it with anyone. It is used to access your rewards, move funds, and derive any information you need in the future.** +### 3. Enter your recovery phrase locally -Run the command below. It prompts for your 24 words and reads them **without echoing** -- the phrase is never passed on the command line, so it stays out of your shell history. +The prompt is hidden. The phrase is used locally to derive your wormhole reward address, then discarded. It is not saved, logged, sent over the network, or placed in command history. -Save the `Inner Hash` from the output: +The installer asks what to call your miner. That name is shown publicly on the telemetry dashboard, so avoid your real name. It then detects a conservative CPU or GPU configuration, starts both processes, and prints status. Initial chain sync downloads and executes every block, which is the only sync mode the node supports. Mainnet is young, so today that is minutes rather than hours, and it grows with the chain. The miner starts working the moment the node reports it is synced. -```sh -./quantus-node key quantus --scheme wormhole --words -``` +## Know when it works -Alternatively, to generate a fresh wallet separate from the one in your wallet app: +Run: ```bash -./quantus-node key quantus --scheme wormhole +./quantus-mining.sh status ``` -Copy the words from the output -- that is your secret phrase. - -**Note: Save this secret phrase securely and do not share with anyone. It is used to access your rewards, move funds, and derive any information you need in the future.** - - -### 4. Start the Node +On Windows, every command is the same with `.\quantus-mining.ps1` in place of `./quantus-mining.sh`. Setup remembers where it installed, so later commands work without setting `QUANTUS_MINING_DIR` or `QUANTUS_NODE_DATA_PATH` again. On Windows, setup also offers to start mining automatically when you sign in. While syncing, the Windows status line also shows the current block, the target, the rate, and the time left. -Replace the two placeholders before running: +A complete success state shows: -- `` -- any name you like (this is how your node appears on [telemetry](https://telemetry.quantus.cat/)) -- `` -- the `inner_hash` value from step 3 +| Field | Ready value | +| --- | --- | +| Network | `mainnet` | +| Compatibility | node `v1.0.1` + miner `v4.2.0` | +| Node | `Running` | +| Sync | `Synced` | +| Miner | `Running` | +| Hash rate | a live rate from the miner | +| Reward address | your public wormhole address | +| Telemetry | your node name at [telemetry.quantus.cat](https://telemetry.quantus.cat/) | -`node_key.p2p` is the file generated in step 2. +Confirm stop-start recovery once: ```bash -./quantus-node \ - --name \ - --validator \ - --miner-listen-port 9833 \ - --chain mainnet \ - --node-key-file node_key.p2p \ - --rewards-inner-hash \ - --max-blocks-per-request 64 \ - --sync full +./quantus-mining.sh restart-check ``` -#### Note on Syncing -Once you begin syncing your node, wait until the node is fully synced before you begin mining. Blocks mined before your node reaches the chain tip are orphans and earn nothing (the miner pauses automatically if your node has no peers). -Sync time grows with the chain: expect anywhere from ~15 minutes to a couple of hours depending on your hardware and connection. Your node is synced when the log switches from `Syncing` to `Idle` at the current tip. +## Daily commands -**Run the node version that matches the network.** If your node stalls mid-sync with `Verification failed` errors and drops to 0 peers, your node version is out of step with the network -- check [Releases](https://github.com/Quantus-Network/chain/releases) and community announcements for which version the network is currently running. +| Goal | Command | +| --- | --- | +| Start or resume | `./quantus-mining.sh mine` | +| Check readiness | `./quantus-mining.sh status` | +| Stop | `./quantus-mining.sh stop` | +| Update to the supported pair, keeping your reward address | `./quantus-mining.sh setup --force` | +| Change resource use | `./quantus-mining.sh config set CPU_WORKERS 4` | +| Windows: start mining when you sign in | `.\quantus-mining.ps1 autostart on` (off: `autostart off`) | -On first start with `--miner-listen-port`, the node writes miner auth material under the chain directory (token is **not** logged -- read the file): - -| File | Purpose | -|------|---------| -| `miner-auth-token` | Shared secret the miner sends in `Ready`. Mode `0600`. Never put this on the command line or in logs. | -| `miner-tls-cert-sha256` | SHA-256 of the miner QUIC cert. Miners must pin this. Also printed in node logs. | -| `miner-tls-cert.der` / `miner-tls-key.der` | Node TLS material (do not copy the private key to miners). | - -Default chain directory: - -| Platform | Path | -|----------|------| -| Linux | `~/.local/share/quantus-node/chains/mainnet/` | -| macOS | `~/Library/Application Support/quantus-node/chains/mainnet/` | - -Wait until logs show the miner server is listening (and the auth/TLS file paths) before starting the miner. If miner-server startup fails, the node exits -- it does not fall back to local mining. - -### 5. Start the Miner - -Download the miner binary from [Miner Releases](https://github.com/Quantus-Network/quantus-miner/releases). Node and miner versions must be a matching pair: the authenticated wire protocol ALPN is `quantus-miner/2`. Confirm `quantus-node --help` lists `--miner-auth-token-file` and `quantus-miner serve --help` lists `--auth-token-file` before using the commands below. A mismatched pair fails at TLS handshake with "no application protocol". Older releases (node v0.9.0, miner v3.3.1 and earlier) do not include miner auth — omit the auth/TLS flags and connect with `--node-addr` only. - -**Open a new terminal window (cmd + t). Let the node run in the original terminal.** - -**macOS only:** - -```bash -xattr -d com.apple.quarantine quantus-miner-macos-aarch64 && chmod u+x quantus-miner-macos-aarch64 -``` - -Wait for the node logs to show the miner server is listening, then run the following in the **separate terminal**. Quote `CHAIN_DIR` — the macOS path contains a space. If not on Apple Silicon, replace `quantus-miner-macos-aarch64` with your platform's binary name. - -```bash -CHAIN_DIR="$HOME/Library/Application Support/quantus-node/chains/mainnet" -# Linux: CHAIN_DIR="$HOME/.local/share/quantus-node/chains/mainnet" - -./quantus-miner-macos-aarch64 serve \ - --cpu-workers 4 \ - --gpu-devices 0 \ - --node-addr 127.0.0.1:9833 \ - --auth-token-file "$CHAIN_DIR/miner-auth-token" \ - --tls-cert-sha256-file "$CHAIN_DIR/miner-tls-cert-sha256" -``` - -Prefer `--auth-token-file` / `--tls-cert-sha256-file` over inline `--auth-token` / `--tls-cert-sha256` so the secret is not stored in shell history. When the node/miner pair includes miner auth, both flags are required; a wrong token or TLS pin is a permanent error (the miner will not reconnect-loop). On pre-auth releases, omit those flags. - -Depending on your machine and resources you can adjust `--gpu-devices` and `--cpu-workers` to see what provides the best balance of hash rate and system usability. - -The above command is fairly conservative for most modern hardware. - -For example if you want to use your GPU and have many CPU cores available you could run - -```bash -CHAIN_DIR="$HOME/Library/Application Support/quantus-node/chains/mainnet" -# Linux: CHAIN_DIR="$HOME/.local/share/quantus-node/chains/mainnet" - -./quantus-miner-macos-aarch64 serve \ - --cpu-workers 8 \ - --gpu-devices 1 \ - --node-addr 127.0.0.1:9833 \ - --auth-token-file "$CHAIN_DIR/miner-auth-token" \ - --tls-cert-sha256-file "$CHAIN_DIR/miner-tls-cert-sha256" -``` - -If the miner exits immediately, it is usually auth or version mismatch: confirm both files exist, that you waited for the miner server to listen, and that node and miner releases match (`quantus-miner/2`). A wrong token or TLS pin is a permanent error -- re-read the files (the token is never logged). - -## Monitoring - -### Your Rewards - -Rewards accumulate at your wormhole address as you mine. The wallet app supports wormhole addresses and encrypted accounts, so if you mine with the same seed phrase as your app wallet, rewards appear in the app and are spendable directly from your wormhole balance -- there is no separate claiming step. - -### Monitoring Your Node -- **Telemetry dashboard:** [telemetry.quantus.cat](https://telemetry.quantus.cat/) -- find your node by name -- **Prometheus metrics (detailed node metrics):** `http://localhost:9615/metrics` -- **RPC endpoint:** `http://localhost:9944` -- **Check your address in the explorer:** Your wormhole address is in the `Address` field from key generation, or in your node's startup logs. - - - -### **Logs & Diagnostics** - -```bash -# Linux -tail -f ~/.local/share/quantus-node/chains/mainnet/network/quantus-node.log - -# macOS -tail -f ~/Library/Application\ Support/quantus-node/chains/mainnet/network/quantus-node.log - -# Or run with verbose logging -RUST_LOG=info ./quantus-node [options] -``` - -#### **Inspect your node's P2P identity:** - -```bash -./quantus-node key inspect-node-key --file node_key.p2p -``` +## Coming from Planck -## Security Best Practices +Planck was the public testnet and is retired. Mainnet does not share its history, database, or balances. -### Key Management +- Run `./quantus-mining.sh setup --force` (or `.\quantus-mining.ps1 setup -Force`). It installs the mainnet pair and keeps your existing reward preimage, address, node name, and resource settings. It does not ask for the recovery phrase again. +- The node syncs mainnet from genesis into `chains/mainnet/`. Planck data under `chains/planck/` is never reused and can be deleted. +- Never pass `--force-authoring` to join mainnet. That flag only bootstraps a brand-new network. -- **Back up your seed phrase securely** +## Security boundary -### Node Security +- Only port `30333` should be public for peer-to-peer networking. +- Keep miner `9833/UDP`, RPC `9944`, and metrics `9615` private and local. +- The miner reads its auth token and TLS pin from local files. Do not paste either value into chat or command arguments. +- The recovery phrase is entered only into the hidden local prompt. +- The reward preimage is stored locally with owner-only permissions because the current node requires it at startup. +- `status` redacts 64-character secret-like values from log excerpts. +- The installer refuses unsupported networks, protocols, release URLs, assets, and checksums. -- **Firewall:** Only expose port 30333 (P2P). Keep 9833/UDP (miner), 9944 (RPC), and 9615 (metrics) on localhost. The miner port binds `0.0.0.0` -- reachability is entirely your firewall. Auth + TLS pinning do **not** make it safe to publish to the internet. -- **Miner secrets:** Treat `miner-auth-token` like a password. Back it up with the same care as other node files; anyone with the token and network access to port 9833 can submit job results and observe mining jobs. -- **Remote miners:** Put node and miners on a private network or VPN (WireGuard, Tailscale, VPC). Do not open 9833/UDP to `0.0.0.0/0`. -- **Updates:** Check [GitHub Releases](https://github.com/Quantus-Network/chain/releases/latest) for new versions regularly. Node and miner must ship the same miner protocol (`quantus-miner/2`). -- **Monitoring:** Watch for unusual peer counts, sync stalls, or dropped miner connections +## Compatibility policy -### Mainnet notes +The machine-readable [compatibility manifest](/mining-compatibility.json) is the release source of truth. The current pair is node `v1.0.1`, the first release carrying the mainnet chain spec, with miner `v4.2.0`. Both were verified together on real hardware against mainnet: the node joins with peers and writes its miner auth files, and the miner authenticates and receives work. The installer does not resolve independent `latest` releases. Node `v1.0.1` publishes no Intel macOS build, so Intel Macs are refused rather than paired with a missing node. -This is the live Quantus network. Tokens (QTC) have value. Back up your seed phrase, keep miner auth files private, and run the node version the network is on. Planck testnet data and PLK balances do not carry over. +The manifest currently publishes minimum OS versions as `not-published`. That is an owner fact still needed from the release team. The manifest does not promise earnings: whether a machine wins blocks depends on its share of total network hash rate. +## Fix one problem at a time -### Getting Help +| Problem | One recovery action | +| --- | --- | +| Checksum failed | Delete the named download and run the verified installer again. | +| Unsupported platform | Use Apple Silicon macOS, Linux x64, or 64-bit Windows 10/11. Intel macOS and Windows on ARM have no published node for the current pair. | +| PowerShell refuses to run the script | Run `Unblock-File .\quantus-mining.ps1`, or start it with `powershell -ExecutionPolicy Bypass -File .\quantus-mining.ps1 mine`. | +| Windows: sync shows peers but the block number is not moving | Check free disk space, peer connectivity and disk activity, then compare block progress with `status`. Do not disable antivirus protection or add exclusions as a default fix. | +| Installed pair is stale, or still on Planck | Run `./quantus-mining.sh setup --force`. Your reward address is kept. | +| Node or miner stopped | Run `./quantus-mining.sh mine`. | +| Sync still says `Syncing` | Leave the process running and check `status` later. | +| macOS blocks a binary | Run `xattr -d com.apple.quarantine ~/quantus-mining/bin/quantus-node ~/quantus-mining/bin/quantus-miner`. | +| No hash rate after sync | Run `status` and check the reported miner/protocol error. If the miner stopped, use `mine`. Do not reinstall or repeat wallet entry without diagnosing the failure. | +| Computer is sluggish | Run `./quantus-mining.sh config set CPU_WORKERS 1`, then `./quantus-mining.sh restart-check`. | -- **GitHub Issues:** [Report bugs](https://github.com/Quantus-Network/chain/issues) -- **Telegram:** [Quantus community](https://t.me/quantusnetwork) -- **Research forum:** [research.quantus.com](https://research.quantus.com) -- deeper technical discussion with the Quantus team \ No newline at end of file +For protocol development and manual commands, use the chain repository's [MINING.md](https://github.com/Quantus-Network/chain/blob/main/MINING.md). Report reproducible defects in [GitHub Issues](https://github.com/Quantus-Network/chain/issues). diff --git a/package.json b/package.json index 41c905f..9d8d2cc 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,8 @@ "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", + "test": "bun test", + "check": "bun run typecheck && bun test && bun run build", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", "typecheck": "tsc" @@ -32,6 +34,7 @@ "@docusaurus/module-type-aliases": "^3.10.2", "@docusaurus/tsconfig": "^3.10.2", "@docusaurus/types": "^3.10.2", + "@types/bun": "^1.4.0", "typescript": "~5.6.2" }, "browserslist": { diff --git a/scripts/test-quantus-mining.sh b/scripts/test-quantus-mining.sh index 6d98f0d..c9f9ab3 100755 --- a/scripts/test-quantus-mining.sh +++ b/scripts/test-quantus-mining.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Regression tests for quantus-mining.sh identity preservation. +# Regression test: setup --force keeps an existing install's reward identity. set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" @@ -24,58 +24,67 @@ PRESERVED_HASH="0xabc123def456abc123def456abc123def456abc123def456abc123def456ab PRESERVED_ADDRESS="qzExistingRewardAddress" PRESERVED_NAME="old-planck-node" -write_existing_config() { +# A Planck-era install: old pair, old chain, preimage in its owner-only file. +write_existing_install() { + mkdir -p "$BIN_DIR" "$LOG_DIR" cat > "$CONFIG_FILE" < "$INNER_HASH_FILE" + chmod 600 "$INNER_HASH_FILE" : > "$NODE_KEY_PATH" } -download_binaries() { - NODE_VERSION="v1.0.1" - MINER_VERSION="v4.1.0" - MINER_PROTOCOL="auth" -} +require_cmd() { :; } detect_platform() { OS="linux" ARCH="x86_64" + PLATFORM_KEY="LinuxX8664" NODE_TARGET="x86_64-unknown-linux-gnu" + MINER_ASSET="quantus-miner-linux-x86_64" +} + +# Stand-in for the verified download: sets what fetching the manifest sets. +download_binaries() { + CHAIN="mainnet" + NODE_VERSION="v1.0.1" + MINER_VERSION="v4.2.0" + MINER_PROTOCOL="auth" } generate_wormhole_keys() { fail "generate_wormhole_keys must not run when refreshing an existing identity" } -prompt_resource_allocation() { - fail "prompt_resource_allocation must not run when refreshing an existing identity" +configure_resource_defaults() { + fail "configure_resource_defaults must not run when refreshing an existing identity" } -write_existing_config -cmd_setup --force +write_existing_install +cmd_setup --force >/dev/null # shellcheck source=/dev/null source "$CONFIG_FILE" -assert_eq "INNER_HASH" "$PRESERVED_HASH" "$INNER_HASH" +assert_eq "rewards-inner-hash" "$PRESERVED_HASH" "$(tr -d '[:space:]' < "$INNER_HASH_FILE")" assert_eq "WORMHOLE_ADDRESS" "$PRESERVED_ADDRESS" "$WORMHOLE_ADDRESS" assert_eq "NODE_NAME" "$PRESERVED_NAME" "$NODE_NAME" assert_eq "CPU_WORKERS" "4" "$CPU_WORKERS" assert_eq "GPU_DEVICES" "1" "$GPU_DEVICES" assert_eq "CHAIN" "mainnet" "$CHAIN" assert_eq "NODE_VERSION" "v1.0.1" "$NODE_VERSION" -assert_eq "MINER_VERSION" "v4.1.0" "$MINER_VERSION" +assert_eq "MINER_VERSION" "v4.2.0" "$MINER_VERSION" +grep -q "INNER_HASH" "$CONFIG_FILE" && fail "the preimage must not be written into the public config" echo "ok: setup --force preserves existing reward identity" diff --git a/src/components/DocActionButtons/index.tsx b/src/components/DocActionButtons/index.tsx index 26cc872..a0d4977 100644 --- a/src/components/DocActionButtons/index.tsx +++ b/src/components/DocActionButtons/index.tsx @@ -27,6 +27,7 @@ export default function DocActionButtons(): ReactNode { const [container, setContainer] = useState(null); const location = useLocation(); const skillUrl = useBaseUrl('/skills/mining-skill.md'); + const compatibilityUrl = useBaseUrl('/mining-compatibility.json'); const isMiningPage = location.pathname.replace(/\/$/, '') === MINING_PATH; @@ -53,16 +54,31 @@ export default function DocActionButtons(): ReactNode { if (isMiningPage) { try { - const res = await fetch(skillUrl); - if (!res.ok) { - throw new Error(`Skill fetch returned ${res.status}`); + const [skillResponse, compatibilityResponse] = await Promise.all([ + fetch(skillUrl), + fetch(compatibilityUrl), + ]); + if (!skillResponse.ok) { + throw new Error(`Skill fetch returned ${skillResponse.status}`); } - const skill = await res.text(); + if (!compatibilityResponse.ok) { + throw new Error( + `Compatibility fetch returned ${compatibilityResponse.status}`, + ); + } + const skill = await skillResponse.text(); + const compatibility = await compatibilityResponse.text(); text = [ '', '', skill.trim(), '', + '## Pinned compatibility manifest', + '', + '```json', + compatibility.trim(), + '```', + '', '---', '', '# Reference: full mining guide (docs.quantus.com/guides/mining)', @@ -81,7 +97,7 @@ export default function DocActionButtons(): ReactNode { await copyTextToClipboard(text); setCopyState('copied'); setTimeout(() => setCopyState('idle'), 2000); - }, [isMiningPage, skillUrl]); + }, [compatibilityUrl, isMiningPage, skillUrl]); if (!container) return null; diff --git a/static/agent-setup/prompt.md b/static/agent-setup/prompt.md new file mode 100644 index 0000000..75c8005 --- /dev/null +++ b/static/agent-setup/prompt.md @@ -0,0 +1,76 @@ +# Set up Quantus mining + +Help the user set up Quantus mainnet mining on this computer. Complete ordinary +setup work with your tools, but pause for private wallet input. Mainnet QTC has +value: the recovery phrase controls real funds. Never set up the retired Planck +testnet, and never promise earnings. + +## Read before running + +Fetch these resources from docs.quantus.com: + +- https://docs.quantus.com/skills/mining-skill.md +- https://docs.quantus.com/mining-compatibility.json +- https://docs.quantus.com/guides/mining/ + +If a resource is missing, returns an HTML error instead of the expected content, +or disagrees with the others, stop and report the exact blocker. Do not invent +commands or substitute a third-party installer. The compatibility manifest is +the source for the supported node/miner pair; never choose independent latest +versions. Read its linked release evidence before installing. + +## Preflight + +1. Detect OS and architecture, free disk space and available CPU/GPU resources. + Use native PowerShell on Windows x64, or the Unix installer on a platform + listed by the guide and manifest. Do not silently install WSL, buy a VPS or + assume phone support. Report unpublished minimum requirements as unknown. +2. Check whether a Quantus setup already exists without reading its secrets. + Preserve existing wallets, configuration and chain data. Do not overwrite or + force-reinstall a working setup without explaining why and asking first. +3. Explain that mining consumes power, storage and compute, and sync can take + time. Use conservative resource settings. Ask before starting a paid cloud + instance or changing firewall, antivirus, privileges or startup services. + Never add antivirus exclusions as an automatic troubleshooting step. + +## Install and start + +Download the matching installer and its published SHA-256 file to a local setup +directory. Verify the bytes before execution and inspect the script. Never pipe +remote code into a shell. A checksum served beside a script checks integrity, +not an independent signature or proof that the code is safe. + +- Windows: https://docs.quantus.com/scripts/quantus-mining.ps1 +- Unix: https://docs.quantus.com/scripts/quantus-mining.sh +- Append `.sha256` to the chosen URL for the checksum file. + +Use the guide's verified command block and the installer's `mine` command. The +installer must validate release downloads against the pinned manifest. Do not +disable verification, supply version overrides or change networks. + +Never ask for or capture a recovery phrase, private key, reward preimage, auth +token or TLS key in chat, command arguments, logs, screenshots or repositories. +Before any wallet prompt, hand control to the user in an unrecorded local +terminal and stop reading it. If your tools cannot provide a private terminal, +prepare the files and ask the user to perform that step locally. Do not run the +interactive wallet step in an agent-captured terminal. Resume only after the +user confirms completion. Do not inspect wallet or credential files. + +Keep miner control, RPC and metrics private. Do not expose ports 9833/UDP, 9944 +or 9615. Only the documented peer-to-peer service may be public. + +## Verify, do not assume + +Use the chosen installer's `status` command. A running process alone is not +success. Distinguish installed, syncing, connected and actively mining states. +Require a synced node, live miner work and a current nonzero hash rate before +reporting mining success. Then run `restart-check` and confirm recovery. + +If sync is unfinished, report "installed, syncing" and the next status command, +not "mining complete". If a check fails, give one specific next action. Do not +automatically weaken security or repeatedly reinstall to hide a failure. + +Finish with non-secret facts: platform, network, pinned versions, sync state, +mining state, hash rate and restart result. Include the local `status` and `stop` +commands for the actual shell. Never paste raw logs or promise earnings, a +launch date, instant sync or a leaderboard position. diff --git a/static/mining-compatibility.json b/static/mining-compatibility.json new file mode 100644 index 0000000..c8525d5 --- /dev/null +++ b/static/mining-compatibility.json @@ -0,0 +1,38 @@ +{ + "schemaVersion": "1", + "updatedAt": "2026-09-14T00:00:00Z", + "status": "supported", + "networkId": "mainnet", + "networkLabel": "Quantus mainnet", + "networkKind": "mainnet", + "tokenValue": "QTC", + "minerProtocol": "quantus-miner/2", + "compatibilityEvidenceUrl": "https://github.com/Quantus-Network/quantus-miner/releases/tag/v4.2.0", + "nodeVersion": "v1.0.1", + "minerVersion": "v4.2.0", + "desktopAppVersion": "miner-v0.6.1", + "desktopAppStatus": "preview", + "desktopAppBundlesRuntime": "false", + "desktopAppVerifiesRuntimeDownloads": "false", + "minimumMacos": "not-published", + "minimumLinux": "not-published", + "minimumWindows": "not-published", + "nodeDarwinArm64Url": "https://github.com/Quantus-Network/chain/releases/download/v1.0.1/quantus-node-v1.0.1-aarch64-apple-darwin.tar.gz", + "nodeDarwinArm64Sha256": "2b60bbc5460d4abad14974086027ad410d19cc3e740054d3abf17481218ee25f", + "minerDarwinArm64Url": "https://github.com/Quantus-Network/quantus-miner/releases/download/v4.2.0/quantus-miner-macos-aarch64", + "minerDarwinArm64Sha256": "b13580e6ae9ed6556f7b163e9ea979e2caaee17a7e1de9cf3ba57eacd6635d1f", + "nodeLinuxX8664Url": "https://github.com/Quantus-Network/chain/releases/download/v1.0.1/quantus-node-v1.0.1-x86_64-unknown-linux-gnu.tar.gz", + "nodeLinuxX8664Sha256": "5880937c2a933b97d9916c5c9e78425d9918c6d7b68abe69de43680f62c13c93", + "minerLinuxX8664Url": "https://github.com/Quantus-Network/quantus-miner/releases/download/v4.2.0/quantus-miner-linux-x86_64", + "minerLinuxX8664Sha256": "a929e11ee1fd4fe291d743f373041bc280620376f5084620db3b0f82d9a2a276", + "nodeWindowsX8664Url": "https://github.com/Quantus-Network/chain/releases/download/v1.0.1/quantus-node-v1.0.1-x86_64-pc-windows-msvc.zip", + "nodeWindowsX8664Sha256": "92d9a5be4fa2efccc4e0f790d38c2a006acc873e026d2483c3eb717d2b2619cc", + "minerWindowsX8664Url": "https://github.com/Quantus-Network/quantus-miner/releases/download/v4.2.0/quantus-miner-windows-x86_64.exe", + "minerWindowsX8664Sha256": "4e807b04ff5e69402a72a527b9e7733b7e88521b6c8c6e8a5021fc13008d2224", + "desktopAppMacosUrl": "https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_macos.zip", + "desktopAppMacosSha256": "fb4769d4c1e8ccd3c480e0caeca07c9baa2b3ef0540828894dddeea3d4ed016e", + "desktopAppLinuxUrl": "https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_linux.tar.gz", + "desktopAppLinuxSha256": "fd2755f2fe5df05af3deb8e1081fa808f6c872bd9d4bf2853eea0fe354655076", + "desktopAppWindowsUrl": "https://github.com/Quantus-Network/quantus-apps/releases/download/miner-v0.6.1/quantus_miner_windows.zip", + "desktopAppWindowsSha256": "081308152ba6f9eba62b9ac143f8c9e115ffab400a978a9d03a996e55ece8ad1" +} diff --git a/static/scripts/Start Quantus Mining.cmd b/static/scripts/Start Quantus Mining.cmd new file mode 100644 index 0000000..9f8a909 --- /dev/null +++ b/static/scripts/Start Quantus Mining.cmd @@ -0,0 +1,2 @@ +@echo off +start "" "%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe" -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "%~dp0quantus-mining-app.ps1" diff --git a/static/scripts/mining.conf.example b/static/scripts/mining.conf.example index 713bd02..b647f5e 100644 --- a/static/scripts/mining.conf.example +++ b/static/scripts/mining.conf.example @@ -1,44 +1,45 @@ # Quantus mining configuration example -# Copy to ~/quantus-mining/mining.conf or run: ./quantus-mining.sh setup +# Written by: ./quantus-mining.sh setup or .\quantus-mining.ps1 setup +# Location: ~/quantus-mining/mining.conf (owner-only permissions) # -# File permissions should be 600 (the setup script sets this automatically). -# Never store your 24-word mnemonic in this file — only INNER_HASH is persisted. +# This file holds public settings only. The reward preimage lives in the +# separate owner-only file ~/quantus-mining/rewards-inner-hash, and the +# 24-word recovery phrase is never written anywhere by the installer. -# Node name shown on https://telemetry.quantus.cat/ -NODE_NAME="my-quantus-node" +RUN_MODE="binary" -# 32-byte wormhole preimage — required for --rewards-inner-hash -INNER_HASH="0x..." +# Node name shown on https://telemetry.quantus.cat/ +NODE_NAME="quantus-my-machine" -# Wormhole SS58 address where mining rewards accumulate (informational) -WORMHOLE_ADDRESS="..." +# Wormhole address where mining rewards accumulate (informational) +WORMHOLE_ADDRESS="qz..." -# P2P node identity file (relative to QUANTUS_MINING_DIR) +# P2P node identity file (relative to the working directory) NODE_KEY_FILE="node_key.p2p" -# Chain spec +# Chain spec. The installers only support mainnet. CHAIN="mainnet" -# QUIC miner server port (node listens; miner connects via --node-addr) -# Auth files live in the node's chain dir (not this file): +# QUIC miner server port (node listens; miner connects via --node-addr). +# Auth files live in the node's chain dir, not here: # miner-auth-token, miner-tls-cert-sha256 MINER_LISTEN_PORT=9833 # External miner resource allocation -# GPU mining recommended: GPU_DEVICES=1, CPU_WORKERS=0 -# CPU-only: leave ~2 cores free for OS/node +# GPU mining: GPU_DEVICES=1, CPU_WORKERS=0 +# CPU-only: GPU_DEVICES=0, CPU_WORKERS= CPU_WORKERS=0 GPU_DEVICES=1 -# Pinned versions. `setup` and `setup --force` download these tags. -# Environment NODE_VERSION / MINER_VERSION override the file. -# Leave empty to use GitHub latest (independent latest tags may not match). -# After changing pins, re-run: ./quantus-mining.sh setup --force -NODE_VERSION="" -MINER_VERSION="" +# The pair the installer verified against the compatibility manifest. +# Not user-editable: start refuses to run if these drift from the manifest. +# To move to a newer published pair, re-run: setup --force +NODE_VERSION="v1.0.1" +MINER_VERSION="v4.2.0" +MINER_PROTOCOL="auth" # Editable via: ./quantus-mining.sh config set KEY VALUE -# NODE_NAME, CPU_WORKERS, GPU_DEVICES, MINER_LISTEN_PORT, CHAIN, -# NODE_VERSION, MINER_VERSION +# NODE_NAME, CPU_WORKERS, GPU_DEVICES, MINER_LISTEN_PORT # -# To change INNER_HASH / rewards destination, re-run setup wormhole keygen. +# To change the reward destination, re-run setup --force and enter the +# recovery phrase for the wallet you want rewards sent to. diff --git a/static/scripts/quantus-mining-app.ps1 b/static/scripts/quantus-mining-app.ps1 new file mode 100644 index 0000000..44edb8d --- /dev/null +++ b/static/scripts/quantus-mining-app.ps1 @@ -0,0 +1,549 @@ +#Requires -Version 5.1 +<# +quantus-mining-app.ps1 - A plain window for Quantus mining on Windows. + +For people who never open a terminal. Everything real is done by +quantus-mining.ps1 (the verified installer): this file only collects two +answers, runs the installer, and turns its status into plain words. + +Start it by double-clicking "Start Quantus Mining.cmd" in the same folder. +#> + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' +Add-Type -AssemblyName System.Windows.Forms, System.Drawing +[System.Windows.Forms.Application]::EnableVisualStyles() + +$script:Here = Split-Path -Parent $PSCommandPath +$script:PowerShellExe = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe' +$script:LocationFile = Join-Path $env:LOCALAPPDATA 'quantus-mining\location' + +# Until docs.quantus.com publishes the manifest, a copy shipped in the same +# download is used. It arrived with the installer, so it carries the same trust. +$bundled = Join-Path $script:Here 'mining-compatibility.json' +if (-not $env:QUANTUS_COMPATIBILITY_URL -and (Test-Path $bundled)) { + $env:QUANTUS_COMPATIBILITY_URL = ([Uri]$bundled).AbsoluteUri +} + +# --------------------------------------------------------------------------- +# Where things are +# --------------------------------------------------------------------------- + +function Get-InstallDir { + if (Test-Path $script:LocationFile) { + $dir = (Get-Content $script:LocationFile | Where-Object { $_ -like 'MINING_DIR=*' }) -replace '^MINING_DIR=', '' + if ($dir -and (Test-Path (Join-Path $dir 'mining.conf'))) { return $dir } + } + $default = Join-Path $HOME 'quantus-mining' + if (Test-Path (Join-Path $default 'mining.conf')) { return $default } + return $null +} + +function Get-Installer { + $dir = Get-InstallDir + if ($dir -and (Test-Path (Join-Path $dir 'quantus-mining.ps1'))) { return (Join-Path $dir 'quantus-mining.ps1') } + $beside = Join-Path $script:Here 'quantus-mining.ps1' + if (Test-Path $beside) { return $beside } + return $null +} + +# Runs one installer command in the background so the window never freezes. +function Start-InstallerJob([string[]]$Arguments) { + Start-Job -ArgumentList $script:PowerShellExe, (Get-Installer), $env:QUANTUS_COMPATIBILITY_URL, $Arguments -ScriptBlock { + param($ps, $installer, $url, $a) + if ($url) { $env:QUANTUS_COMPATIBILITY_URL = $url } + & $ps -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $installer @a 2>&1 | Out-String + } +} + +# --------------------------------------------------------------------------- +# Plain words +# --------------------------------------------------------------------------- + +function Get-Field([string]$Text, [string]$Name) { + $m = [regex]::Match($Text, "(?m)^$Name\:\s*(.+)$") + if ($m.Success) { return $m.Groups[1].Value.Trim() } + return '' +} + +function ConvertTo-PlainStatus([string]$Text) { + $overall = Get-Field $Text 'Overall' + $sync = Get-Field $Text 'Sync' + $rate = Get-Field $Text 'Hash rate' + if ($overall -eq 'MINING') { + return @{ Title = 'Mining'; Detail = "Speed: $rate. Rewards go to your Quantus Wallet app."; Color = [Drawing.Color]::FromArgb(22, 128, 60); Running = $true } + } + if ($overall -eq 'STOPPED') { + return @{ Title = 'Not mining'; Detail = 'Press Start to begin.'; Color = [Drawing.Color]::FromArgb(90, 90, 90); Running = $false } + } + if ($sync -like 'Stalled*') { + return @{ Title = 'Stuck'; Detail = 'Not catching up with the network. Check the internet, then restart the laptop if it stays like this.'; Color = [Drawing.Color]::FromArgb(180, 60, 20); Running = $true } + } + $pct = [regex]::Match($sync, '\((\d+(?:\.\d+)?)%\)') + $left = [regex]::Match($sync, 'about (.+?) left') + if ($pct.Success) { + $detail = "Catching up with the network: $($pct.Groups[1].Value)%" + if ($left.Success) { $detail += " (about $($left.Groups[1].Value) left)" } + return @{ Title = 'Getting ready'; Detail = "$detail. Mining starts by itself."; Color = [Drawing.Color]::FromArgb(40, 90, 170); Running = $true } + } + if ($rate -like '*not working now*') { + return @{ Title = 'Paused'; Detail = 'The miner stopped working. Press Stop, then Start.'; Color = [Drawing.Color]::FromArgb(180, 60, 20); Running = $true } + } + return @{ Title = 'Starting'; Detail = 'This takes about a minute.'; Color = [Drawing.Color]::FromArgb(40, 90, 170); Running = $true } +} + + +# --------------------------------------------------------------------------- +# Earnings: the reward address's real balance, read from this laptop's own node +# --------------------------------------------------------------------------- + +# Base58 and BLAKE2b, needed to turn the qz... address into the chain's storage +# key. Kept as source text so the background job can compile the same code. +$script:ChainCodec = @' +using System; +using System.Numerics; +public static class QuantusCodec { + const string Alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"; + public static byte[] Base58Decode(string s) { + BigInteger n = 0; + foreach (char c in s) { int d = Alphabet.IndexOf(c); if (d < 0) throw new FormatException("not base58"); n = n * 58 + d; } + byte[] le = n.ToByteArray(); Array.Reverse(le); + int skip = 0; while (skip < le.Length - 1 && le[skip] == 0) skip++; + int zeros = 0; while (zeros < s.Length && s[zeros] == '1') zeros++; + byte[] r = new byte[zeros + le.Length - skip]; + Array.Copy(le, skip, r, zeros, le.Length - skip); + return r; + } + static readonly ulong[] IV = { 0x6a09e667f3bcc908UL, 0xbb67ae8584caa73bUL, 0x3c6ef372fe94f82bUL, 0xa54ff53a5f1d36f1UL, 0x510e527fade682d1UL, 0x9b05688c2b3e6c1fUL, 0x1f83d9abfb41bd6bUL, 0x5be0cd19137e2179UL }; + static readonly int[,] Sigma = { + {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15},{14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3},{11,8,12,0,5,2,15,13,10,14,3,6,7,1,9,4}, + {7,9,3,1,13,12,11,14,2,6,5,10,4,0,15,8},{9,0,5,7,2,4,10,15,14,1,11,12,6,8,3,13},{2,12,6,10,0,11,8,3,4,13,7,5,15,14,1,9}, + {12,5,1,15,14,13,4,10,0,7,6,3,9,2,8,11},{13,11,7,14,12,1,3,9,5,0,15,4,8,6,2,10},{6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5}, + {10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0} }; + static ulong Rot(ulong x, int n) { return (x >> n) | (x << (64 - n)); } + public static byte[] Blake2b(byte[] data, int outLen) { + ulong[] h = (ulong[])IV.Clone(); h[0] ^= 0x01010000UL ^ (ulong)outLen; + ulong t = 0; int off = 0; int len = data.Length; + do { + byte[] block = new byte[128]; int take = Math.Min(128, len - off); + Array.Copy(data, off, block, 0, take); off += take; t += (ulong)take; + bool last = off >= len; + ulong[] m = new ulong[16]; for (int i = 0; i < 16; i++) m[i] = BitConverter.ToUInt64(block, i * 8); + ulong[] v = new ulong[16]; Array.Copy(h, v, 8); Array.Copy(IV, 0, v, 8, 8); + v[12] ^= t; if (last) v[14] = ~v[14]; + for (int r = 0; r < 12; r++) { + int[] idx = { 0,4,8,12, 1,5,9,13, 2,6,10,14, 3,7,11,15, 0,5,10,15, 1,6,11,12, 2,7,8,13, 3,4,9,14 }; + for (int g = 0; g < 8; g++) { + int a = idx[g*4], b = idx[g*4+1], c = idx[g*4+2], d = idx[g*4+3]; + ulong x = m[Sigma[r % 10, 2*g]], y = m[Sigma[r % 10, 2*g+1]]; + v[a] = v[a] + v[b] + x; v[d] = Rot(v[d] ^ v[a], 32); v[c] = v[c] + v[d]; v[b] = Rot(v[b] ^ v[c], 24); + v[a] = v[a] + v[b] + y; v[d] = Rot(v[d] ^ v[a], 16); v[c] = v[c] + v[d]; v[b] = Rot(v[b] ^ v[c], 63); + } + } + for (int i = 0; i < 8; i++) h[i] ^= v[i] ^ v[i + 8]; + if (last) break; + } while (true); + byte[] outb = new byte[outLen]; + for (int i = 0; i < outLen; i++) outb[i] = (byte)(h[i / 8] >> (8 * (i % 8))); + return outb; + } + static string Hex(byte[] b) { return BitConverter.ToString(b).Replace("-", "").ToLowerInvariant(); } + // System.Account storage key for an SS58 address, or null if the checksum fails. + public static string AccountKey(string address) { + byte[] raw = Base58Decode(address); + if (raw.Length != 35 && raw.Length != 36) return null; + int prefix = raw.Length - 34; + byte[] body = new byte[raw.Length - 2]; Array.Copy(raw, body, body.Length); + byte[] pre = System.Text.Encoding.ASCII.GetBytes("SS58PRE"); + byte[] withPre = new byte[pre.Length + body.Length]; pre.CopyTo(withPre, 0); body.CopyTo(withPre, pre.Length); + byte[] sum = Blake2b(withPre, 64); + if (sum[0] != raw[raw.Length - 2] || sum[1] != raw[raw.Length - 1]) return null; + byte[] acct = new byte[32]; Array.Copy(raw, prefix, acct, 0, 32); + return "0x26aa394eea5630e07c48ae0c9558cef7b99d880ec681799c0cf30e8886371da9" + Hex(Blake2b(acct, 16)) + Hex(acct); + } +} +'@ + +# Runs in a background job: balance from the local node, price from CoinGecko. +$script:EarningsScript = { + param($codec, $conf) + $out = @{ Qtc = $null; Usd = $null } + try { + Add-Type -TypeDefinition $codec -ReferencedAssemblies System.Numerics -ErrorAction Stop + $addr = ((Get-Content $conf) | Where-Object { $_ -like 'WORMHOLE_ADDRESS=*' }) -replace '^WORMHOLE_ADDRESS=', '' -replace '"', '' + $key = [QuantusCodec]::AccountKey($addr.Trim()) + if ($key) { + $body = '{"jsonrpc":"2.0","id":1,"method":"state_getStorage","params":["' + $key + '"]}' + $v = (Invoke-RestMethod http://127.0.0.1:9944 -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 5).result + # AccountInfo: nonce, consumers, providers, sufficients (4 x u32), then free balance as u128. + if (-not $v) { $out.Qtc = 0.0 } else { + $bytes = [byte[]]::new(16) + for ($i = 0; $i -lt 16; $i++) { $bytes[$i] = [Convert]::ToByte($v.Substring(2 + (16 + $i) * 2, 2), 16) } + $free = [Numerics.BigInteger]::new([byte[]]($bytes + [byte]0)) + $out.Qtc = [double]([decimal]$free / 1000000000000) + } + } + } catch { } + try { + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + $out.Usd = [double](Invoke-RestMethod 'https://api.coingecko.com/api/v3/simple/price?ids=quantus&vs_currencies=usd' -TimeoutSec 6).quantus.usd + } catch { } + [pscustomobject]$out +} + +function Format-Earnings($e) { + $minerProc = Get-Process quantus-miner -ErrorAction SilentlyContinue | Select-Object -First 1 + $since = '' + if ($minerProc) { + $span = (Get-Date) - $minerProc.StartTime + $since = if ($span.TotalHours -ge 1) { ' · Mining for {0} h {1} min' -f [int][math]::Floor($span.TotalHours), $span.Minutes } else { ' · Mining for {0} min' -f [int]$span.TotalMinutes } + } + if ($null -eq $e -or $null -eq $e.Qtc) { return "Earned: checking...$since" } + $qtc = if ($e.Qtc -eq 0) { '0' } else { '{0:N4}' -f $e.Qtc } + $usd = if ($null -ne $e.Usd) { ' ({0:C2})' -f ($e.Qtc * $e.Usd) } else { '' } + return "Rewards wallet: $qtc QTC$usd$since" +} + +function Get-CoolerSetting { + $dir = Get-InstallDir + if (-not $dir) { return $false } + $line = (Get-Content (Join-Path $dir 'mining.conf')) | Where-Object { $_ -like 'GPU_THROTTLE_MS=*' } + return ($line -and [int]($line -replace '^GPU_THROTTLE_MS=', '') -gt 0) +} + +function Get-HeatWarning { + $smi = Get-Command nvidia-smi -ErrorAction SilentlyContinue + if (-not $smi) { return '' } + try { + $out = & $smi.Source --query-gpu=temperature.gpu,clocks_event_reasons.hw_thermal_slowdown,clocks_event_reasons.sw_thermal_slowdown --format=csv,noheader 2>$null + if ($out -match 'Active') { return 'Your laptop is hot, so mining is slower. Raise the back of the laptop and keep the air vents clear.' } + } catch { } + return '' +} + +function ConvertTo-PlainSetupLine([string]$Line) { + switch -Regex ($Line) { + '^Storage:' { return 'Checking there is enough space...' } + 'Downloading quantus-node' { return 'Downloading the Quantus software (1 of 2)...' } + 'Downloading quantus-miner' { return 'Downloading the Quantus software (2 of 2)...' } + 'Verified SHA-256' { return 'Checking the download is genuine...' } + 'Reward address:' { return 'Wallet connected.' } + 'Starting quantus-node' { return 'Starting...' } + 'Waiting for miner' { return 'Starting...' } + 'Miner started' { return 'Started.' } + } + return $null +} + +function Test-MinerName([string]$Name) { return ($Name -cmatch '^[a-z0-9](?:[a-z0-9-]{1,30})[a-z0-9]$') } + +# --------------------------------------------------------------------------- +# Window +# --------------------------------------------------------------------------- + +$font = New-Object Drawing.Font('Segoe UI', 10) +$form = New-Object Windows.Forms.Form +$form.Text = 'Quantus Mining' +$form.Size = New-Object Drawing.Size(520, 500) +$form.StartPosition = 'CenterScreen' +$form.FormBorderStyle = 'FixedDialog' +$form.MaximizeBox = $false +$form.Font = $font +$form.BackColor = [Drawing.Color]::White + +function New-Label([string]$Text, [int]$Y, [int]$Size = 10, [string]$Style = 'Regular', [int]$Height = 24) { + $l = New-Object Windows.Forms.Label + $l.Text = $Text + $l.Location = New-Object Drawing.Point(28, $Y) + $l.Size = New-Object Drawing.Size(450, $Height) + $l.Font = New-Object Drawing.Font('Segoe UI', $Size, [Drawing.FontStyle]$Style) + return $l +} + +# ----- setup panel ----- +$setup = New-Object Windows.Forms.Panel +$setup.Dock = 'Fill' + +$setup.Controls.Add((New-Label 'Mine Quantus on this laptop' 20 16 'Bold' 34)) +$setup.Controls.Add((New-Label 'Open the Quantus Wallet app on your phone and have your 24 secret words ready.' 60 10 'Regular' 42)) + +$setup.Controls.Add((New-Label 'Your 24 secret words' 108 10 'Bold')) +$words = New-Object Windows.Forms.TextBox +$words.Location = New-Object Drawing.Point(28, 134) +$words.Size = New-Object Drawing.Size(450, 28) +$words.UseSystemPasswordChar = $true +$setup.Controls.Add($words) + +$show = New-Object Windows.Forms.CheckBox +$show.Text = 'Show the words while I type' +$show.Location = New-Object Drawing.Point(28, 166) +$show.Size = New-Object Drawing.Size(300, 24) +$show.Add_CheckedChanged({ $words.UseSystemPasswordChar = -not $show.Checked }) +$setup.Controls.Add($show) + +$setup.Controls.Add((New-Label 'The words stay on this laptop. Quantus never asks for them by message, email or phone call.' 192 9 'Regular' 36)) + +$setup.Controls.Add((New-Label 'Miner name (shown publicly, so not your real name)' 234 10 'Bold')) +$name = New-Object Windows.Forms.TextBox +$name.Location = New-Object Drawing.Point(28, 260) +$name.Size = New-Object Drawing.Size(450, 28) +$hostPart = ($env:COMPUTERNAME.ToLowerInvariant() -replace '[^a-z0-9-]', '').Trim('-') +$name.Text = if ($hostPart) { "quantus-$hostPart" } else { 'quantus-miner' } +$setup.Controls.Add($name) + +$go = New-Object Windows.Forms.Button +$go.Text = 'Start mining' +$go.Location = New-Object Drawing.Point(28, 306) +$go.Size = New-Object Drawing.Size(450, 44) +$go.Font = New-Object Drawing.Font('Segoe UI', 12, [Drawing.FontStyle]::Bold) +$go.BackColor = [Drawing.Color]::FromArgb(255, 107, 53) +$go.ForeColor = [Drawing.Color]::White +$go.FlatStyle = 'Flat' +$setup.Controls.Add($go) + +$progress = New-Label '' 360 10 'Regular' 40 +$setup.Controls.Add($progress) + +# ----- status panel ----- +$status = New-Object Windows.Forms.Panel +$status.Dock = 'Fill' +$status.Visible = $false + +$title = New-Label 'Checking...' 30 26 'Bold' 50 +$status.Controls.Add($title) +$detail = New-Label '' 86 11 'Regular' 44 +$status.Controls.Add($detail) +$earn = New-Label 'Earned: checking...' 134 11 'Bold' 26 +$status.Controls.Add($earn) +$heat = New-Label '' 166 10 'Regular' 44 +$heat.ForeColor = [Drawing.Color]::FromArgb(180, 60, 20) +$status.Controls.Add($heat) + +$start = New-Object Windows.Forms.Button +$start.Text = 'Start' +$start.Location = New-Object Drawing.Point(28, 220) +$start.Size = New-Object Drawing.Size(215, 44) +$status.Controls.Add($start) + +$stop = New-Object Windows.Forms.Button +$stop.Text = 'Stop' +$stop.Location = New-Object Drawing.Point(263, 220) +$stop.Size = New-Object Drawing.Size(215, 44) +$status.Controls.Add($stop) + +$auto = New-Object Windows.Forms.CheckBox +$auto.Text = 'Start mining when I turn on the laptop' +$auto.Location = New-Object Drawing.Point(28, 280) +$auto.Size = New-Object Drawing.Size(450, 28) +$status.Controls.Add($auto) + +$cooler = New-Object Windows.Forms.CheckBox +$cooler.Text = 'Run cooler (a little slower, easier on the laptop)' +$cooler.Location = New-Object Drawing.Point(28, 312) +$cooler.Size = New-Object Drawing.Size(450, 28) +$status.Controls.Add($cooler) + +$status.Controls.Add((New-Label 'You can close this window. Mining keeps going.' 380 9 'Regular')) + +$form.Controls.Add($status) +$form.Controls.Add($setup) + +# --------------------------------------------------------------------------- +# Behaviour +# --------------------------------------------------------------------------- + +$script:StatusJob = $null +$script:ActionJob = $null +$script:SetupProc = $null +$script:SetupLines = [Collections.ArrayList]::Synchronized((New-Object Collections.ArrayList)) +$script:AutoLoaded = $false +$script:EarningsJob = $null +$script:Earnings = $null +$script:NextEarnings = [DateTime]::MinValue +# Pause between GPU batches when "Run cooler" is on, chosen by measurement. +$script:CoolerMs = '30' + +function Show-Status { + $setup.Visible = $false + $status.Visible = $true + if (-not $script:AutoLoaded) { + $out = & $script:PowerShellExe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File (Get-Installer) autostart status 2>&1 | Out-String + $auto.Checked = ($out -match 'Autostart: on') + $cooler.Checked = Get-CoolerSetting + $script:AutoLoaded = $true + } +} + +$cooler.Add_Click({ + $ms = if ($cooler.Checked) { $script:CoolerMs } else { '0' } + $title.Text = 'Restarting'; $detail.Text = 'Applying the new setting. This takes about a minute.' + $start.Enabled = $false; $stop.Enabled = $false + $script:ActionJob = Start-Job -ArgumentList $script:PowerShellExe, (Get-Installer), $env:QUANTUS_COMPATIBILITY_URL, $ms -ScriptBlock { + param($ps, $installer, $url, $ms) + if ($url) { $env:QUANTUS_COMPATIBILITY_URL = $url } + & $ps -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $installer config set GPU_THROTTLE_MS $ms 2>&1 | Out-Null + & $ps -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $installer restart 2>&1 | Out-String + } +}) + +$auto.Add_Click({ + $script:ActionJob = Start-InstallerJob @('autostart', $(if ($auto.Checked) { 'on' } else { 'off' })) +}) + +$start.Add_Click({ + $title.Text = 'Starting'; $detail.Text = 'This takes about a minute.' + $start.Enabled = $false + $script:ActionJob = Start-InstallerJob @('start') +}) + +$stop.Add_Click({ + $title.Text = 'Stopping...'; $detail.Text = '' + $stop.Enabled = $false + $script:ActionJob = Start-InstallerJob @('stop') +}) + +$go.Add_Click({ + $phrase = (($words.Text -split '\s+') | Where-Object { $_ }) -join ' ' + $count = ($phrase -split ' ').Count + if (-not $phrase -or $count -ne 24) { + [Windows.Forms.MessageBox]::Show("Please type all 24 secret words, with a space between each one. You typed $(if ($phrase) { $count } else { 0 }).", 'Quantus Mining') | Out-Null + return + } + $miner = $name.Text.Trim().ToLowerInvariant() + if (-not (Test-MinerName $miner)) { + [Windows.Forms.MessageBox]::Show('Please use 3 to 32 lowercase letters, numbers and hyphens for the miner name.', 'Quantus Mining') | Out-Null + return + } + $installer = Get-Installer + if (-not $installer) { + [Windows.Forms.MessageBox]::Show('quantus-mining.ps1 is missing. Keep all the downloaded files together in one folder.', 'Quantus Mining') | Out-Null + return + } + + $go.Enabled = $false; $words.Enabled = $false; $name.Enabled = $false + $progress.Text = 'Getting started...' + $script:SetupLines.Clear() + + $psi = New-Object Diagnostics.ProcessStartInfo + $psi.FileName = $script:PowerShellExe + $psi.Arguments = "-NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$installer`" mine" + $psi.UseShellExecute = $false + $psi.CreateNoWindow = $true + $psi.RedirectStandardInput = $true + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.EnvironmentVariables['QUANTUS_NODE_NAME'] = $miner + if ($env:QUANTUS_COMPATIBILITY_URL) { $psi.EnvironmentVariables['QUANTUS_COMPATIBILITY_URL'] = $env:QUANTUS_COMPATIBILITY_URL } + + $proc = New-Object Diagnostics.Process + $proc.StartInfo = $psi + $handler = { if ($EventArgs.Data) { $Event.MessageData.Add($EventArgs.Data) | Out-Null } } + Register-ObjectEvent -InputObject $proc -EventName OutputDataReceived -Action $handler -MessageData $script:SetupLines | Out-Null + Register-ObjectEvent -InputObject $proc -EventName ErrorDataReceived -Action $handler -MessageData $script:SetupLines | Out-Null + $proc.Start() | Out-Null + $proc.BeginOutputReadLine() + $proc.BeginErrorReadLine() + # The phrase is written once to the installer's standard input and dropped. + $proc.StandardInput.WriteLine($phrase) + $proc.StandardInput.Close() + $phrase = $null + $words.Text = '' + $script:SetupProc = $proc +}) + +$timer = New-Object Windows.Forms.Timer +$timer.Interval = 2000 +$script:NextStatus = [DateTime]::MinValue +$timer.Add_Tick({ + # Setup in progress + if ($script:SetupProc) { + foreach ($line in @($script:SetupLines.ToArray())) { + $plain = ConvertTo-PlainSetupLine $line + if ($plain) { $progress.Text = $plain } + } + if ($script:SetupProc.HasExited) { + $code = $script:SetupProc.ExitCode + $script:SetupProc = $null + Get-EventSubscriber | Unregister-Event + if ($code -eq 0) { + & $script:PowerShellExe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File (Get-Installer) autostart on 2>&1 | Out-Null + Install-Shortcut + Show-Status + $script:NextStatus = [DateTime]::MinValue + } else { + $err = @($script:SetupLines.ToArray()) | Where-Object { $_ -like 'Error:*' } | Select-Object -Last 1 + $msg = if ($err) { $err -replace '^Error:\s*', '' } else { 'Setup did not finish.' } + [Windows.Forms.MessageBox]::Show("Mining could not start.`n`n$msg", 'Quantus Mining') | Out-Null + $progress.Text = '' + $go.Enabled = $true; $words.Enabled = $true; $name.Enabled = $true + } + } + return + } + + if (-not $status.Visible) { return } + + if ($script:ActionJob -and $script:ActionJob.State -ne 'Running') { + Receive-Job $script:ActionJob -ErrorAction SilentlyContinue | Out-Null + Remove-Job $script:ActionJob -Force + $script:ActionJob = $null + $start.Enabled = $true; $stop.Enabled = $true + $script:NextStatus = [DateTime]::MinValue + } + + if ($script:StatusJob -and $script:StatusJob.State -ne 'Running') { + $text = (Receive-Job $script:StatusJob -ErrorAction SilentlyContinue | Out-String) + Remove-Job $script:StatusJob -Force + $script:StatusJob = $null + $p = ConvertTo-PlainStatus $text + $title.Text = $p.Title + $title.ForeColor = $p.Color + $detail.Text = $p.Detail + $heat.Text = Get-HeatWarning + $earn.Text = Format-Earnings $script:Earnings + if (-not $script:ActionJob) { $start.Enabled = -not $p.Running; $stop.Enabled = $p.Running } + } + + if (-not $script:StatusJob -and -not $script:ActionJob -and (Get-Date) -ge $script:NextStatus) { + $script:StatusJob = Start-InstallerJob @('status') + $script:NextStatus = (Get-Date).AddSeconds(15) + } + + if ($script:EarningsJob -and $script:EarningsJob.State -ne 'Running') { + $r = Receive-Job $script:EarningsJob -ErrorAction SilentlyContinue | Select-Object -Last 1 + Remove-Job $script:EarningsJob -Force + $script:EarningsJob = $null + if ($r) { $script:Earnings = $r; $earn.Text = Format-Earnings $r } + } + if (-not $script:EarningsJob -and (Get-Date) -ge $script:NextEarnings) { + $conf = Join-Path (Get-InstallDir) 'mining.conf' + $script:EarningsJob = Start-Job -ScriptBlock $script:EarningsScript -ArgumentList $script:ChainCodec, $conf + $script:NextEarnings = (Get-Date).AddMinutes(1) + } +}) + +# A desktop icon that opens this window, pointing at the copy kept beside the +# install so it survives the download folder being cleaned out. +function Install-Shortcut { + $dir = Get-InstallDir + if (-not $dir) { return } + foreach ($f in @('quantus-mining-app.ps1', 'mining-compatibility.json')) { + $src = Join-Path $script:Here $f + if ((Test-Path $src) -and ([IO.Path]::GetFullPath($src) -ne [IO.Path]::GetFullPath((Join-Path $dir $f)))) { Copy-Item -LiteralPath $src -Destination $dir -Force } + } + $shell = New-Object -ComObject WScript.Shell + $link = $shell.CreateShortcut((Join-Path ([Environment]::GetFolderPath('Desktop')) 'Quantus Mining.lnk')) + $link.TargetPath = $script:PowerShellExe + $link.Arguments = "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$(Join-Path $dir 'quantus-mining-app.ps1')`"" + $link.WorkingDirectory = $dir + $link.Description = 'Quantus Mining' + $icon = Join-Path $dir 'bin\quantus-miner.exe' + if (Test-Path $icon) { $link.IconLocation = "$icon,0" } + $link.Save() +} + +if (Get-InstallDir) { Show-Status } +$timer.Start() +[void]$form.ShowDialog() +$timer.Stop() +Get-Job | Remove-Job -Force -ErrorAction SilentlyContinue diff --git a/static/scripts/quantus-mining.ps1 b/static/scripts/quantus-mining.ps1 new file mode 100644 index 0000000..9c9e09d --- /dev/null +++ b/static/scripts/quantus-mining.ps1 @@ -0,0 +1,1113 @@ +#Requires -Version 5.1 +<# +quantus-mining.ps1 - Set up and manage verified Quantus mainnet mining on Windows. + +Native Windows twin of quantus-mining.sh. Same commands, same compatibility +manifest, same checksums, same hidden recovery-phrase prompt, same status +contract. Requires Windows 10/11 x64 and PowerShell 5.1 or later. No WSL. + +Default working directory: $HOME\quantus-mining\ +Config file: $HOME\quantus-mining\mining.conf (owner-only) + +Usage: + .\quantus-mining.ps1 mine + .\quantus-mining.ps1 setup [-Force] + .\quantus-mining.ps1 config show|set KEY VALUE + .\quantus-mining.ps1 start + .\quantus-mining.ps1 stop|restart + .\quantus-mining.ps1 status|restart-check + .\quantus-mining.ps1 uninstall [-Force] + .\quantus-mining.ps1 help + +If PowerShell refuses to run the file, it was downloaded with a web mark. Run: + Unblock-File .\quantus-mining.ps1 +or start it as: + powershell -ExecutionPolicy Bypass -File .\quantus-mining.ps1 mine + +Override directory: $env:QUANTUS_MINING_DIR = 'D:\path'; .\quantus-mining.ps1 ... +#> + +[CmdletBinding()] +param( + [Parameter(Position = 0)][string]$Command = 'help', + [Parameter(Position = 1, ValueFromRemainingArguments = $true)][string[]]$Rest = @(), + [switch]$Force +) + +Set-StrictMode -Version 2.0 +$ErrorActionPreference = 'Stop' + +# Started from a PowerShell 7 window, Windows PowerShell inherits that window's +# module paths, then cannot load its own built-in modules (Get-FileHash, +# Expand-Archive, the CIM commands) and setup dies mid-download. Keep only the +# paths that belong to this edition. +if ($PSVersionTable.PSEdition -eq 'Desktop' -and $env:PSModulePath) { + $own = @("$PSHOME\Modules", (Join-Path $env:ProgramFiles 'WindowsPowerShell\Modules')) + $kept = @($env:PSModulePath -split ';' | Where-Object { $_ -and $_ -notlike '*\Program Files\PowerShell\*' -and $_ -notlike '*microsoft.powershell_*' -and $_ -notlike '*\Documents\PowerShell\*' }) + $env:PSModulePath = (@($kept) + ($own | Where-Object { $kept -notcontains $_ })) -join ';' +} + +# --------------------------------------------------------------------------- +# Paths and constants +# --------------------------------------------------------------------------- + +$script:ScriptName = 'quantus-mining.ps1' + +# Where the last setup installed to, so every later command finds it without +# the user retyping QUANTUS_MINING_DIR / QUANTUS_NODE_DATA_PATH. Environment +# variables still win. The file holds two paths and nothing secret. +$script:LocationFile = Join-Path $env:LOCALAPPDATA 'quantus-mining\location' +if (-not $env:QUANTUS_MINING_DIR -and (Test-Path $script:LocationFile)) { + foreach ($line in Get-Content $script:LocationFile) { + if ($line -notmatch '^(MINING_DIR|NODE_DATA_PATH)=(.+)$') { continue } + $value = $Matches[2].Trim() + if (-not [IO.Path]::IsPathRooted($value) -or $value -match "['`"]") { continue } + if ($Matches[1] -eq 'MINING_DIR') { $env:QUANTUS_MINING_DIR = $value } + elseif (-not $env:QUANTUS_NODE_DATA_PATH) { $env:QUANTUS_NODE_DATA_PATH = $value } + } +} + +# Per-user login entry. No administrator rights needed. Overridable only so the +# tests can exercise it without touching the real startup list. +$script:AutostartKey = if ($env:QUANTUS_AUTOSTART_KEY) { $env:QUANTUS_AUTOSTART_KEY } else { 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' } +$script:AutostartName = 'QuantusMining' + +$script:MiningDir = if ($env:QUANTUS_MINING_DIR) { $env:QUANTUS_MINING_DIR } else { Join-Path $HOME 'quantus-mining' } +$script:ConfigFile = Join-Path $script:MiningDir 'mining.conf' +$script:BinDir = Join-Path $script:MiningDir 'bin' +$script:LogDir = Join-Path $script:MiningDir 'logs' +$script:NodeBin = Join-Path $script:BinDir 'quantus-node.exe' +$script:MinerBin = Join-Path $script:BinDir 'quantus-miner.exe' +$script:NodePidFile = Join-Path $script:MiningDir 'node.pid' +$script:MinerPidFile = Join-Path $script:MiningDir 'miner.pid' +$script:NodeKeyPath = Join-Path $script:MiningDir 'node_key.p2p' +$script:InnerHashFile = Join-Path $script:MiningDir 'rewards-inner-hash' +$script:CompatibilityFile = Join-Path $script:MiningDir 'mining-compatibility.json' + +# Every hint prints a command that can be pasted as-is. PowerShell will not run a +# script by bare name, so "quantus-mining.ps1 stop" fails; the full invocation +# of the installed copy (or this file, before setup) does not. +$script:RunTarget = Join-Path $script:MiningDir $script:ScriptName +if (-not (Test-Path $script:RunTarget) -and $PSCommandPath) { $script:RunTarget = $PSCommandPath } +$script:RunHint = "powershell -ExecutionPolicy Bypass -File `"$($script:RunTarget)`"" +$script:CompatibilityUrl = if ($env:QUANTUS_COMPATIBILITY_URL) { $env:QUANTUS_COMPATIBILITY_URL } else { 'https://docs.quantus.com/mining-compatibility.json' } + +$script:ChainRepo = 'Quantus-Network/chain' +$script:MinerRepo = 'Quantus-Network/quantus-miner' +$script:EditableKeys = @('NODE_NAME', 'CPU_WORKERS', 'GPU_DEVICES', 'MINER_LISTEN_PORT', 'GPU_THROTTLE_MS') + +# Only x64 Windows has published release assets. ARM64 Windows can run the +# x64 binaries under emulation, and the miner would still not see the GPU +# natively, so it is refused rather than half-supported. +$script:PlatformKey = 'WindowsX8664' +$script:NodeTarget = 'x86_64-pc-windows-msvc' +$script:MinerAsset = 'quantus-miner-windows-x86_64.exe' + +$script:Manifest = @{} +$script:Config = @{} +# 'auth' or 'legacy', decided by probing both binaries' --help. Declared here so +# strict mode never sees it unset; every path that reads it runs the probe first. +$script:MinerProtocol = '' + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +# Native programs write progress and results to stderr. Under Stop, Windows +# PowerShell turns any redirected stderr line into a terminating error, which +# only shows up when this script's own output is captured (the desktop app, an +# agent). Native calls run under Continue and are judged by their exit code. +function Invoke-Native([scriptblock]$Block) { + $saved = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { & $Block } finally { $ErrorActionPreference = $saved } +} + +function Fail([string]$Message) { + [Console]::Error.WriteLine("Error: $Message") + exit 1 +} + +function Info([string]$Message) { Write-Output $Message } + +function Warn([string]$Message) { [Console]::Error.WriteLine("Warning: $Message") } + +function Test-Platform { + if (-not [Environment]::Is64BitOperatingSystem) { + Fail 'Unsupported platform: 32-bit Windows. Use a 64-bit Windows 10/11 machine.' + } + $arch = $env:PROCESSOR_ARCHITECTURE + if ($arch -ne 'AMD64') { + Fail "Unsupported architecture: $arch. Quantus publishes Windows binaries for x64 only." + } +} + +function Ensure-Dirs { + foreach ($d in @($script:MiningDir, $script:BinDir, $script:LogDir)) { + if (-not (Test-Path $d)) { New-Item -ItemType Directory -Force $d | Out-Null } + } +} + +# Owner-only file, the Windows equivalent of chmod 600. Inheritance is +# removed first so the parent folder's Users entry does not leak through. +function Protect-File([string]$Path) { + $me = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name + & icacls $Path /inheritance:r /grant:r "${me}:(F)" | Out-Null +} + +function Get-CpuCount { + try { return [int](Get-CimInstance Win32_ComputerSystem).NumberOfLogicalProcessors } catch { return 4 } +} + +function Get-DiscreteGpuPresent { + # The miner skips integrated adapters by default when a discrete one exists, + # so this only decides the default of GPU_DEVICES, not which device is used. + try { + $names = (Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join ' ' + return ($names -match 'NVIDIA|GeForce|Quadro|RTX|GTX|Radeon RX|Radeon Pro|Arc') + } catch { return $false } +} + +function Get-FileSha256([string]$Path) { + return (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToLowerInvariant() +} + +function Assert-Sha256([string]$Path, [string]$Expected, [string]$Label = '') { + $name = if ($Label) { $Label } else { Split-Path $Path -Leaf } + if (-not $Expected -or $Expected -notmatch '^[0-9a-f]{64}$') { + Fail "Invalid SHA-256 value for $name. No files were installed." + } + $actual = Get-FileSha256 $Path + if ($actual -ne $Expected) { + Remove-Item $Path -Force -ErrorAction SilentlyContinue + Fail ("Checksum verification failed for {0}.`nExpected: {1}`nActual: {2}`nThe file was not installed. Delete the download and retry." -f $name, $Expected, $actual) + } + Info "Verified SHA-256: $name" +} + +function Assert-ReleaseUrl([string]$Url, [string]$Repo, [string]$Version) { + $prefix = "https://github.com/$Repo/releases/download/$Version/" + if (-not $Url.StartsWith($prefix)) { + Fail "Compatibility manifest contains an unexpected download URL for $Repo. No files were installed." + } +} + +function Invoke-Download([string]$Url, [string]$OutFile) { + # TLS 1.2 is not the default on older PowerShell 5.1 hosts. + [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + try { + Invoke-WebRequest -Uri $Url -OutFile $OutFile -UseBasicParsing + } catch { + Fail "Failed to download $Url. Check your connection and retry." + } +} + +# --------------------------------------------------------------------------- +# Compatibility manifest +# --------------------------------------------------------------------------- + +function Read-ManifestString([hashtable]$Manifest, [string]$Key) { + if (-not $Manifest.ContainsKey($Key) -or [string]::IsNullOrWhiteSpace([string]$Manifest[$Key])) { + Fail "Compatibility manifest is missing $Key. No files were installed." + } + return [string]$Manifest[$Key] +} + +function Import-CompatibilityManifest([string]$Path) { + if (-not (Test-Path $Path) -or (Get-Item $Path).Length -eq 0) { + Fail "Compatibility manifest not found at $Path. Run: $($script:RunHint) setup -Force" + } + $raw = Get-Content $Path -Raw + try { $json = $raw | ConvertFrom-Json } catch { Fail 'Compatibility manifest is not valid JSON. No files were installed.' } + $m = @{} + foreach ($p in $json.PSObject.Properties) { $m[$p.Name] = [string]$p.Value } + + $status = Read-ManifestString $m 'status' + if ($status -ne 'supported') { + Fail "Mining compatibility status is '$status', not 'supported'. Nothing will be installed or started." + } + $m['_chain'] = Read-ManifestString $m 'networkId' + $kind = Read-ManifestString $m 'networkKind' + $token = Read-ManifestString $m 'tokenValue' + $m['_nodeVersion'] = Read-ManifestString $m 'nodeVersion' + $m['_minerVersion'] = Read-ManifestString $m 'minerVersion' + $m['_minerProtocol'] = Read-ManifestString $m 'minerProtocol' + $evidence = Read-ManifestString $m 'compatibilityEvidenceUrl' + + if ($m['_chain'] -ne 'mainnet' -or $kind -ne 'mainnet' -or -not $token) { + Fail 'This installer is restricted to Quantus mainnet. The manifest requested a different network.' + } + if ($m['_minerProtocol'] -ne 'quantus-miner/2') { + Fail "Unsupported miner protocol '$($m['_minerProtocol'])'. Nothing will be installed or started." + } + if (-not $evidence.StartsWith('https://github.com/Quantus-Network/quantus-miner/releases/tag/')) { + Fail 'Compatibility evidence URL is not an official Quantus miner release.' + } + + $m['_nodeUrl'] = Read-ManifestString $m "node$($script:PlatformKey)Url" + $m['_nodeSha'] = Read-ManifestString $m "node$($script:PlatformKey)Sha256" + $m['_minerUrl'] = Read-ManifestString $m "miner$($script:PlatformKey)Url" + $m['_minerSha'] = Read-ManifestString $m "miner$($script:PlatformKey)Sha256" + + Assert-ReleaseUrl $m['_nodeUrl'] $script:ChainRepo $m['_nodeVersion'] + Assert-ReleaseUrl $m['_minerUrl'] $script:MinerRepo $m['_minerVersion'] + $expectedNodeAsset = "quantus-node-$($m['_nodeVersion'])-$($script:NodeTarget).zip" + if ((Split-Path $m['_nodeUrl'] -Leaf) -ne $expectedNodeAsset) { + Fail 'Node asset does not match windows/x64. No files were installed.' + } + if ((Split-Path $m['_minerUrl'] -Leaf) -ne $script:MinerAsset) { + Fail 'Miner asset does not match windows/x64. No files were installed.' + } + $script:Manifest = $m + return $m +} + +function Get-CompatibilityManifest { + $temp = "$($script:CompatibilityFile).download" + Info 'Fetching the supported mainnet release pair...' + Invoke-Download $script:CompatibilityUrl $temp + Import-CompatibilityManifest $temp | Out-Null + Move-Item $temp $script:CompatibilityFile -Force + Protect-File $script:CompatibilityFile + Info "Supported pair: node $($script:Manifest['_nodeVersion']) + miner $($script:Manifest['_minerVersion']) ($($script:Manifest['_minerProtocol']))" +} + +# --------------------------------------------------------------------------- +# Pair protocol probe. Same test the shell installer runs. +# --------------------------------------------------------------------------- + +function Get-MinerProtocol([string]$NodeHelp, [string]$MinerHelp) { + $nodeAuth = $NodeHelp.Contains('miner-auth-token-file') + $minerAuth = $MinerHelp.Contains('auth-token-file') -and $MinerHelp.Contains('tls-cert-sha256-file') + if ($nodeAuth -and $minerAuth) { return 'auth' } + if (-not $nodeAuth -and -not $minerAuth) { return 'legacy' } + Fail ("Incompatible node/miner pair for the miner protocol.`n Node --miner-auth-token-file: {0}`n Miner --auth-token-file / --tls-cert-sha256-file: {1}`n`nDo not mix a release that requires Ready {{ token }} (quantus-miner/2) with one that does not.`nRun {2} setup -Force to reinstall the supported pair from:`n {3}" -f $nodeAuth, $minerAuth, $script:ScriptName, $script:CompatibilityUrl) +} + +function Test-InstalledPair { + if (-not (Test-Path $script:NodeBin)) { Fail "quantus-node not found at $($script:NodeBin)" } + if (-not (Test-Path $script:MinerBin)) { Fail "quantus-miner not found at $($script:MinerBin)" } + $nodeHelp = Invoke-Native { & $script:NodeBin --help 2>&1 | Out-String } + if ($LASTEXITCODE -ne 0) { Fail "Failed to probe quantus-node --help (exit $LASTEXITCODE)." } + $minerHelp = Invoke-Native { & $script:MinerBin serve --help 2>&1 | Out-String } + if ($LASTEXITCODE -ne 0) { Fail "Failed to probe quantus-miner serve --help (exit $LASTEXITCODE)." } + $script:MinerProtocol = Get-MinerProtocol $nodeHelp $minerHelp + Info "Miner protocol: $($script:MinerProtocol)" +} + +# --------------------------------------------------------------------------- +# Downloads +# --------------------------------------------------------------------------- + +function Install-NodeBinary { + $url = $script:Manifest['_nodeUrl']; $sha = $script:Manifest['_nodeSha'] + Info "Downloading quantus-node $($script:Manifest['_nodeVersion']) for $($script:NodeTarget)..." + $temp = Join-Path ([IO.Path]::GetTempPath()) ("quantus-node-" + [Guid]::NewGuid().ToString('N')) + New-Item -ItemType Directory -Force $temp | Out-Null + $zip = Join-Path $temp (Split-Path $url -Leaf) + Invoke-Download $url $zip + Assert-Sha256 $zip $sha + Expand-Archive -Path $zip -DestinationPath $temp -Force + $exe = Get-ChildItem $temp -Recurse -Filter 'quantus-node.exe' | Select-Object -First 1 + if (-not $exe) { Remove-Item $temp -Recurse -Force; Fail 'quantus-node.exe not found in archive' } + Copy-Item $exe.FullName $script:NodeBin -Force + Remove-Item $temp -Recurse -Force + Info "Installed quantus-node to $($script:NodeBin)" +} + +function Install-MinerBinary { + $url = $script:Manifest['_minerUrl']; $sha = $script:Manifest['_minerSha'] + Info "Downloading quantus-miner $($script:Manifest['_minerVersion']) ($($script:MinerAsset))..." + $temp = "$($script:MinerBin).download" + Invoke-Download $url $temp + Assert-Sha256 $temp $sha $script:MinerAsset + Move-Item $temp $script:MinerBin -Force + Info "Installed quantus-miner to $($script:MinerBin)" +} + +function Install-Binaries { + Get-CompatibilityManifest + Install-NodeBinary + Install-MinerBinary + Test-InstalledPair + if ($script:MinerProtocol -ne 'auth') { + Fail 'The downloaded binaries do not implement the manifest protocol quantus-miner/2. Nothing will be started.' + } +} + +# --------------------------------------------------------------------------- +# Wallet +# --------------------------------------------------------------------------- + +function Read-WormholeOutput([string]$Output) { + $addr = ($Output -split "`r?`n" | Where-Object { $_ -match '^\s*Address:\s*(\S+)' } | ForEach-Object { $Matches[1] } | Select-Object -First 1) + $hash = ($Output -split "`r?`n" | Where-Object { $_ -match '^\s*Inner [Hh]ash:\s*(\S+)' } | ForEach-Object { $Matches[1] } | Select-Object -First 1) + if (-not $hash) { + $hash = ($Output -split "`r?`n" | Where-Object { $_ -match '^\s*inner_hash:\s*(\S+)' } | ForEach-Object { $Matches[1] } | Select-Object -First 1) + } + if (-not $addr) { Fail 'Could not parse wormhole Address from keygen output' } + if (-not $hash) { Fail 'Could not parse Inner Hash from keygen output' } + return @{ Address = $addr; InnerHash = $hash } +} + +function New-WormholeKeys { + Write-Output '' + Info 'Wallet step: enter your existing Quantus 24-word recovery phrase locally.' + Info 'Input is hidden and is not written to disk, logs, command arguments, or network requests.' + Info 'Never paste a recovery phrase into chat or a support ticket.' + if ([Console]::IsInputRedirected) { + # The desktop launcher collects the phrase in a masked box and hands it over + # on standard input, the same channel the node reads it from below. It never + # appears in arguments, environment variables, files or logs. + $phrase = [Console]::In.ReadLine() + } else { + $secure = Read-Host -Prompt 'Recovery phrase' -AsSecureString + $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) + try { + $phrase = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) + } finally { + [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) + } + } + if ([string]::IsNullOrWhiteSpace($phrase)) { Fail 'Recovery phrase cannot be empty. Open your Quantus wallet backup and retry.' } + + # The phrase goes to the node on stdin only, never as an argument. + $output = Invoke-Native { $phrase | & $script:NodeBin key quantus --scheme wormhole --words 2>&1 | Out-String } + $phrase = $null + $keys = Read-WormholeOutput $output + $output = $null + + Set-Content -Path $script:InnerHashFile -Value $keys.InnerHash -NoNewline -Encoding ascii + Protect-File $script:InnerHashFile + $script:Config['WORMHOLE_ADDRESS'] = $keys.Address + + Write-Output '' + Info "Reward address: $($keys.Address)" + Info 'Your recovery phrase was not saved. Keep your existing offline backup.' +} + +# --------------------------------------------------------------------------- +# Config +# --------------------------------------------------------------------------- + +function Set-ResourceDefaults { + $cores = Get-CpuCount + $workers = [Math]::Max(1, $cores - 2) + if (Get-DiscreteGpuPresent) { + $script:Config['GPU_DEVICES'] = '1' + $script:Config['CPU_WORKERS'] = '0' + Info 'Mining resources: one detected GPU, CPU reserved for the node.' + } else { + $script:Config['GPU_DEVICES'] = '0' + $script:Config['CPU_WORKERS'] = [string]$workers + Info "Mining resources: $workers CPU workers, two cores reserved when available." + } +} + +function Write-Config { + $c = $script:Config + if (-not $c.ContainsKey('CHAIN')) { $c['CHAIN'] = 'mainnet' } + if (-not $c.ContainsKey('MINER_LISTEN_PORT')) { $c['MINER_LISTEN_PORT'] = '9833' } + if (-not $c.ContainsKey('CPU_WORKERS')) { $c['CPU_WORKERS'] = '0' } + if (-not $c.ContainsKey('GPU_DEVICES')) { $c['GPU_DEVICES'] = '0' } + $lines = @( + "# Quantus mining configuration - $($script:ConfigFile)", + "# Generated by $($script:ScriptName) setup", + '', + 'RUN_MODE=binary', + "NODE_NAME=$($c['NODE_NAME'])", + "WORMHOLE_ADDRESS=$($c['WORMHOLE_ADDRESS'])", + 'NODE_KEY_FILE=node_key.p2p', + "CHAIN=$($c['CHAIN'])", + "MINER_LISTEN_PORT=$($c['MINER_LISTEN_PORT'])", + "CPU_WORKERS=$($c['CPU_WORKERS'])", + "GPU_DEVICES=$($c['GPU_DEVICES'])", + "GPU_THROTTLE_MS=$($c['GPU_THROTTLE_MS'])", + "NODE_VERSION=$($c['NODE_VERSION'])", + "MINER_VERSION=$($c['MINER_VERSION'])", + "MINER_PROTOCOL=$($c['MINER_PROTOCOL'])" + ) + Set-Content -Path $script:ConfigFile -Value ($lines -join "`r`n") -Encoding ascii + Protect-File $script:ConfigFile + Info "Wrote config to $($script:ConfigFile)" +} + +function Read-Config { + if (-not (Test-Path $script:ConfigFile)) { Fail "Config not found at $($script:ConfigFile). Run: $($script:RunHint) setup" } + $c = @{} + foreach ($line in Get-Content $script:ConfigFile) { + if ($line -match '^\s*#' -or $line -notmatch '=') { continue } + $k, $v = $line -split '=', 2 + $c[$k.Trim()] = $v.Trim().Trim('"') + } + if (-not $c.ContainsKey('NODE_NAME')) { Fail 'NODE_NAME missing in config' } + if (-not (Test-Path $script:InnerHashFile) -or (Get-Item $script:InnerHashFile).Length -eq 0) { + Fail "Reward preimage file is missing. Re-run $($script:RunHint) setup -Force and enter the phrase locally." + } + $c['_innerHash'] = (Get-Content $script:InnerHashFile -Raw).Trim() + if (-not $c['_innerHash']) { Fail "Reward preimage file is empty. Re-run $($script:RunHint) setup -Force." } + foreach ($pair in @(@('CHAIN', 'mainnet'), @('MINER_LISTEN_PORT', '9833'), @('CPU_WORKERS', '0'), @('GPU_DEVICES', '0'), @('GPU_THROTTLE_MS', '0'), @('NODE_KEY_FILE', 'node_key.p2p'))) { + if (-not $c.ContainsKey($pair[0])) { $c[$pair[0]] = $pair[1] } + } + $script:Config = $c + return $c +} + +# --------------------------------------------------------------------------- +# Processes +# --------------------------------------------------------------------------- + +function Get-NodeDataPath { + if ($env:QUANTUS_NODE_DATA_PATH) { return $env:QUANTUS_NODE_DATA_PATH } + return Join-Path $env:LOCALAPPDATA 'quantus-node' +} + +function Get-AvailableStorageBytes { + $path = [IO.Path]::GetFullPath((Get-NodeDataPath)) + $root = [IO.Path]::GetPathRoot($path) + $drive = New-Object IO.DriveInfo($root) + if (-not $drive.IsReady) { Fail "Data drive is not ready: $root" } + return $drive.AvailableFreeSpace +} + +function Test-SetupStorage { + # The pinned node v1.0.1 MINING.md specifies 100 GB available for setup. + $required = 100000000000L + try { $available = Get-AvailableStorageBytes } catch { Fail 'Cannot measure free storage. Choose an available local data drive using QUANTUS_NODE_DATA_PATH.' } + if ($available -lt $required) { + $gb = [math]::Round($available / 1000000000, 1) + Fail "Storage: $gb GB free; 100 GB required for setup. Free space or set QUANTUS_NODE_DATA_PATH to a larger drive. No downloads or wallet input were started." + } + Info 'Storage: at least 100 GB free on the selected data drive.' +} + +function Get-NodeChainDir { return Join-Path (Get-NodeDataPath) "chains\$($script:Config['CHAIN'])" } +function Get-MinerAuthTokenPath { return Join-Path (Get-NodeChainDir) 'miner-auth-token' } +function Get-MinerTlsPinPath { return Join-Path (Get-NodeChainDir) 'miner-tls-cert-sha256' } + +function Read-PidFile([string]$Path) { + if (Test-Path $Path) { return (Get-Content $Path -Raw).Trim() } + return '' +} + +function Test-ProcessAlive([string]$procId, [string]$ExpectedName = '') { + if (-not $procId) { return $false } + $p = Get-Process -Id ([int]$procId) -ErrorAction SilentlyContinue + if (-not $p) { return $false } + if ($ExpectedName -and $p.ProcessName -ne $ExpectedName) { return $false } + return $true +} + +function Stop-Tracked([string]$PidFile, [string]$ProcessName, [string]$Label) { + $stopped = $false + $procId = Read-PidFile $PidFile + if (Test-ProcessAlive $procId $ProcessName) { + Info "Stopping $Label (PID $procId)..." + Stop-Process -Id ([int]$procId) -Force -ErrorAction SilentlyContinue + $stopped = $true + } + # Any stray copy started from this install's bin dir is ours too. + foreach ($p in Get-Process -Name $ProcessName -ErrorAction SilentlyContinue) { + try { + if ($p.Path -and $p.Path.StartsWith($script:BinDir, [StringComparison]::OrdinalIgnoreCase)) { + Info "Stopping $Label (PID $($p.Id))..." + Stop-Process -Id $p.Id -Force -ErrorAction SilentlyContinue + $stopped = $true + } + } catch { } + } + Remove-Item $PidFile -Force -ErrorAction SilentlyContinue + return $stopped +} + +function Test-MinerPortListening([int]$Port) { + # QUIC is UDP. The TCP check is kept for parity with the shell installer. + $udp = Get-NetUDPEndpoint -LocalPort $Port -ErrorAction SilentlyContinue + if ($udp) { return $true } + $tcp = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue + return [bool]$tcp +} + +function Test-StackRunning { + if (Test-ProcessAlive (Read-PidFile $script:NodePidFile) 'quantus-node') { return $true } + if (Test-ProcessAlive (Read-PidFile $script:MinerPidFile) 'quantus-miner') { return $true } + return (Test-MinerPortListening ([int]$script:Config['MINER_LISTEN_PORT'])) +} + +function Get-NodeLaunchArgs { + $c = $script:Config + return @( + '--name', $c['NODE_NAME'], + '--validator', + '--base-path', (Get-NodeDataPath), + '--miner-listen-port', $c['MINER_LISTEN_PORT'], + '--chain', $c['CHAIN'], + '--node-key-file', (Join-Path $script:MiningDir $c['NODE_KEY_FILE']), + '--rewards-inner-hash', $c['_innerHash'], + '--max-blocks-per-request', '64', + '--sync', 'full' + ) +} + +function Get-MinerLaunchArgs { + $c = $script:Config + $launch = @( + 'serve', + '--cpu-workers', $c['CPU_WORKERS'], + '--gpu-devices', $c['GPU_DEVICES'], + '--node-addr', "127.0.0.1:$($c['MINER_LISTEN_PORT'])" + ) + # A pause between GPU batches. It lowers heat and power at some cost in speed, + # which on a laptop that is already thermal-throttling costs less than it looks. + if ($c.ContainsKey('GPU_THROTTLE_MS') -and [int]$c['GPU_THROTTLE_MS'] -gt 0) { $launch += @('--gpu-throttle-ms', $c['GPU_THROTTLE_MS']) } + if ($script:MinerProtocol -eq 'auth') { + $token = Get-MinerAuthTokenPath; $pin = Get-MinerTlsPinPath + if (-not (Test-Path $token)) { Fail "Miner auth token not found at $token. Start the node first and wait until it is listening." } + if (-not (Test-Path $pin)) { Fail "Miner TLS pin not found at $pin. Start the node first and wait until it is listening." } + $launch += @('--auth-token-file', $token, '--tls-cert-sha256-file', $pin) + } + return $launch +} + +function Wait-ForMinerServer([int]$Port, [string]$NodeLog, [int]$Timeout = 120) { + Info "Waiting for miner server on port $Port (up to ${Timeout}s)..." + for ($i = 0; $i -lt $Timeout; $i++) { + if ((Test-Path $NodeLog) -and (Select-String -Path $NodeLog -Pattern 'Miner server listening' -Quiet)) { return } + if (Test-MinerPortListening $Port) { return } + Start-Sleep 1 + } + Fail "Timed out waiting for miner server on port $Port. Check $NodeLog" +} + +function Wait-ForMinerAuthFiles([int]$Timeout = 30) { + if ($script:MinerProtocol -ne 'auth') { return } + Info "Waiting for miner auth files (up to ${Timeout}s)..." + for ($i = 0; $i -lt $Timeout; $i++) { + if ((Test-Path (Get-MinerAuthTokenPath)) -and (Test-Path (Get-MinerTlsPinPath))) { return } + Start-Sleep 1 + } + Fail ("Timed out waiting for miner auth files:`n {0}`n {1}`nStart the node first and wait until it is listening." -f (Get-MinerAuthTokenPath), (Get-MinerTlsPinPath)) +} + +# Launches through WMI rather than Start-Process. Start-Process with redirection +# hands this script's own standard handles down to the child. When the script's +# output is captured (an agent, CI, anything reading it through a pipe), the +# long-running node and miner then hold that pipe open, and the caller waits for +# it forever even though start has finished. A WMI-created process inherits +# nothing, and cmd.exe does the log redirection on the far side. +function Start-Hidden([string]$Exe, [string[]]$Arguments, [string]$Log) { + $quoted = ($Arguments | ForEach-Object { if ($_ -match '[\s"]') { '"' + ($_ -replace '"', '\"') + '"' } else { $_ } }) -join ' ' + $commandLine = 'cmd.exe /d /s /c ""{0}" {1} 1>"{2}.out" 2>"{2}""' -f $Exe, $quoted, $Log + $startup = New-CimInstance -ClassName Win32_ProcessStartup -ClientOnly -Property @{ ShowWindow = [uint16]0 } + $created = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $commandLine; ProcessStartupInformation = $startup } + if ($created.ReturnValue -ne 0) { Fail "Could not start $(Split-Path $Exe -Leaf) (WMI error $($created.ReturnValue))." } + $name = Split-Path $Exe -Leaf + for ($i = 0; $i -lt 50; $i++) { + $child = Get-CimInstance Win32_Process -Filter "ParentProcessId = $($created.ProcessId)" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq $name } | Select-Object -First 1 + if ($child) { return (Get-Process -Id $child.ProcessId) } + if (-not (Get-Process -Id $created.ProcessId -ErrorAction SilentlyContinue)) { break } + Start-Sleep -Milliseconds 100 + } + Fail "$name exited immediately. Check $Log" +} + +function Confirm-StartPrerequisites { + Read-Config | Out-Null + Ensure-Dirs + Test-Platform + Import-CompatibilityManifest $script:CompatibilityFile | Out-Null + $c = $script:Config; $m = $script:Manifest + if ($c['CHAIN'] -eq 'planck') { Warn 'CHAIN=planck is the retired public testnet. Planck chain data is a different network and is not reused.' } + if ($c['NODE_VERSION'] -ne $m['_nodeVersion'] -or $c['MINER_VERSION'] -ne $m['_minerVersion'] -or $c['CHAIN'] -ne $m['_chain']) { + Fail ("Installed mining files do not match the supported manifest.`nInstalled: node {0} + miner {1} on {2}`nRequired: node {3} + miner {4} on {5}`nRun: {6} setup -Force" -f $c['NODE_VERSION'], $c['MINER_VERSION'], $c['CHAIN'], $m['_nodeVersion'], $m['_minerVersion'], $m['_chain'], $script:ScriptName) + } + if (-not (Test-Path $script:NodeBin)) { Fail "quantus-node not found at $($script:NodeBin). Run: $($script:RunHint) setup" } + if (-not (Test-Path $script:MinerBin)) { Fail "quantus-miner not found at $($script:MinerBin). Run: $($script:RunHint) setup" } + $key = Join-Path $script:MiningDir $c['NODE_KEY_FILE'] + if (-not (Test-Path $key)) { Fail "Node key not found at $key. Run: $($script:RunHint) setup" } + Test-InstalledPair +} + +# --------------------------------------------------------------------------- +# Status helpers +# --------------------------------------------------------------------------- + +# mining.conf values are also shown publicly (the node name, on telemetry) and +# passed to the node on its command line, so every editable value is checked. +function Test-NodeName([string]$Name) { + return ($Name -cmatch '^[a-z0-9](?:[a-z0-9-]{1,30})[a-z0-9]$') +} + +function Test-ConfigValue([string]$Key, [string]$Value) { + switch ($Key) { + 'NODE_NAME' { return (Test-NodeName $Value) } + { $_ -in 'CPU_WORKERS', 'GPU_DEVICES' } { return ($Value -match '^\d{1,3}$' -and [int]$Value -le 256) } + 'GPU_THROTTLE_MS' { return ($Value -match '^\d{1,4}$' -and [int]$Value -le 1000) } + 'MINER_LISTEN_PORT' { return ($Value -match '^\d{4,5}$' -and [int]$Value -ge 1024 -and [int]$Value -le 65535) } + default { return $false } + } +} + +function Get-DefaultNodeName { + $h = ($env:COMPUTERNAME.ToLowerInvariant() -replace '[^a-z0-9-]', '').Trim('-') + if ($h.Length -gt 24) { $h = $h.Substring(0, 24).Trim('-') } + if ($h -and (Test-NodeName "quantus-$h")) { return "quantus-$h" } + return 'quantus-miner' +} + +# Asks for the public telemetry name. QUANTUS_NODE_NAME answers it without a +# prompt, and a non-interactive session (an agent, CI) takes the default instead +# of blocking on input nobody can give. +function Select-NodeName { + $default = Get-DefaultNodeName + if ($env:QUANTUS_NODE_NAME) { + if (-not (Test-NodeName $env:QUANTUS_NODE_NAME)) { Fail 'QUANTUS_NODE_NAME must be 3-32 lowercase letters, digits and hyphens.' } + return $env:QUANTUS_NODE_NAME + } + if (-not [Environment]::UserInteractive -or [Console]::IsInputRedirected) { return $default } + Write-Output '' + Info "Your miner's name is shown publicly on https://telemetry.quantus.cat/." + Info 'Use lowercase letters, numbers and hyphens. Avoid your real name.' + while ($true) { + $answer = Read-Host "Miner name [$default]" + $answer = if ([string]::IsNullOrWhiteSpace($answer)) { $default } else { $answer.Trim().ToLowerInvariant() } + if (Test-NodeName $answer) { return $answer } + Warn 'Use 3-32 lowercase letters, numbers and hyphens, not starting or ending with a hyphen.' + } +} + +function Save-InstallLocation { + $dir = Split-Path $script:LocationFile -Parent + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force $dir | Out-Null } + $lines = @("MINING_DIR=$([IO.Path]::GetFullPath($script:MiningDir))", "NODE_DATA_PATH=$([IO.Path]::GetFullPath((Get-NodeDataPath)))") + Set-Content -Path $script:LocationFile -Value ($lines -join "`r`n") -Encoding ascii +} + +# A copy of this exact script beside the install, so autostart and later +# commands do not depend on wherever the user happened to download it. +function Copy-InstalledScript { + $target = Join-Path $script:MiningDir $script:ScriptName + if (-not $PSCommandPath) { return } + if ([IO.Path]::GetFullPath($PSCommandPath) -eq [IO.Path]::GetFullPath($target)) { return } + Copy-Item -LiteralPath $PSCommandPath -Destination $target -Force +} + +function Get-AutostartCommand { + $script_ = Join-Path $script:MiningDir $script:ScriptName + $log = Join-Path $script:LogDir 'autostart.log' + if ($script_ -match "'" -or $log -match "'") { Fail 'The install path contains a quote character, which autostart cannot handle. Reinstall to a simpler path.' } + # The short wait lets a USB data drive and the network come up after sign-in. + return "powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -Command `"Start-Sleep 20; & '$script_' start *>> '$log'`"" +} + +function Get-AutostartValue { + try { return (Get-ItemProperty -Path $script:AutostartKey -Name $script:AutostartName -ErrorAction Stop).$($script:AutostartName) } catch { return $null } +} + +function Invoke-Autostart([string[]]$AutostartArgs) { + $sub = if ($AutostartArgs.Count -gt 0) { $AutostartArgs[0] } else { 'status' } + switch ($sub) { + 'on' { + if (-not (Test-Path $script:ConfigFile)) { Fail "Mining is not configured. Run: $($script:RunHint) mine" } + Copy-InstalledScript + if (-not (Test-Path $script:AutostartKey)) { New-Item -Path $script:AutostartKey -Force | Out-Null } + Set-ItemProperty -Path $script:AutostartKey -Name $script:AutostartName -Value (Get-AutostartCommand) + Info 'Autostart: on. Mining will start about 20 seconds after you sign in to Windows.' + Info "Turn it off with: $($script:RunHint) autostart off" + } + 'off' { + if (Get-AutostartValue) { Remove-ItemProperty -Path $script:AutostartKey -Name $script:AutostartName } + Info 'Autostart: off. Mining will not start by itself after sign-in.' + } + 'status' { + $v = Get-AutostartValue + if ($v) { Info 'Autostart: on' } else { Info "Autostart: off (turn on with: $($script:RunHint) autostart on)" } + } + default { Fail "Usage: $($script:RunHint) autostart on|off|status" } + } +} + +# Turns the miner's log into one plain number. Each GPU worker logs every search +# as "GPU worker N ...: H hashes in T s". A single search can be two seconds +# long when a new block cuts it short, so one line swings widely. The rate shown +# is total hashes over total seconds per worker in the last three minutes, +# summed across workers. Nothing in that window means the miner is not working +# now, even if its process is still alive. +function Get-HashRateSummary([string]$MinerLog, [int]$WindowMinutes = 3) { + $result = @{ Text = 'Waiting for miner output'; Fresh = $false } + if (-not (Test-Path $MinerLog)) { return $result } + $pattern = '^\[(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})Z[^\]]*\]\s*GPU worker (\d+)\b.*?(\d+) hashes in (\d+(?:\.\d+)?)s' + $cutoff = (Get-Date).ToUniversalTime().AddMinutes(-$WindowMinutes) + $perWorker = @{} + $lastSeen = $null + foreach ($m in (Select-String -Path $MinerLog -Pattern $pattern | Select-Object -Last 2000)) { + $g = $m.Matches[0].Groups + $when = [DateTime]::SpecifyKind([DateTime]::ParseExact($g[1].Value, 'yyyy-MM-ddTHH:mm:ss', [Globalization.CultureInfo]::InvariantCulture), [DateTimeKind]::Utc) + $lastSeen = $when + if ($when -lt $cutoff) { continue } + $w = $g[2].Value + if (-not $perWorker.ContainsKey($w)) { $perWorker[$w] = @{ H = [double]0; S = [double]0 } } + $perWorker[$w].H += [double]$g[3].Value + $perWorker[$w].S += [double]$g[4].Value + } + $total = 0.0 + foreach ($v in $perWorker.Values) { if ($v.S -gt 0) { $total += $v.H / $v.S } } + if ($total -gt 0) { + $result.Fresh = $true + $result.Text = if ($total -ge 1e9) { '{0:N1} GH/s' -f ($total / 1e9) } elseif ($total -ge 1e6) { '{0:N1} MH/s' -f ($total / 1e6) } elseif ($total -ge 1e3) { '{0:N1} KH/s' -f ($total / 1e3) } else { '{0:N0} H/s' -f $total } + return $result + } + if ($lastSeen) { + $result.Text = 'no hashes in the last {0} min (last seen {1:N0} min ago), miner is not working now' -f $WindowMinutes, ((Get-Date).ToUniversalTime() - $lastSeen).TotalMinutes + } + return $result +} + +function Invoke-NodeRpc([string]$Method) { + $body = '{"jsonrpc":"2.0","id":1,"method":"' + $Method + '","params":[]}' + try { + $r = Invoke-RestMethod -Uri 'http://127.0.0.1:9944' -Method Post -ContentType 'application/json' -Body $body -TimeoutSec 3 + return $r.result + } catch { return $null } +} + +function Hide-Secrets([string]$Text) { + $t = $Text -replace '(?i)(recovery phrase|seed phrase|mnemonic|private key|inner hash|auth token)([=:]\s*|\s+)\S+', '$1: [redacted]' + $t = $t -replace '0x[0-9a-fA-F]{64}', '[redacted-hex]' + $t = $t -replace '[0-9a-fA-F]{64}', '[redacted-hex]' + return $t +} + +function Format-Eta([double]$Seconds) { + if ($Seconds -lt 60) { return '{0:N0}s' -f $Seconds } + if ($Seconds -lt 3600) { return '{0:N0} min' -f ($Seconds / 60) } + return '{0:N1} h' -f ($Seconds / 3600) +} + +# --------------------------------------------------------------------------- +# Commands +# --------------------------------------------------------------------------- + +function Invoke-Help { + @" +$($script:ScriptName) - Set up and manage verified Quantus mainnet mining on Windows. + +Working directory: $($script:MiningDir) +Config file: $($script:ConfigFile) + +Commands: + preflight Check Windows platform and setup storage without wallet input + mine Set up if needed, start in background, and show status + setup [-Force] Interactive setup: download binaries, generate keys, write config + config show Show non-secret configuration + config set KEY VALUE Update an editable config key + start Start node + miner in the background + stop Stop node, miner, and related helper processes + restart Stop then start + restart-check Restart in background and verify both processes + status Show a redacted mining readiness summary + uninstall [-Force] Stop processes and remove $($script:MiningDir) (config, keys, binaries, logs) + autostart on|off|status Start mining automatically when you sign in to Windows + help Show this help + +Editable config keys: $($script:EditableKeys -join ' ') + +Environment: + QUANTUS_MINING_DIR Override default working directory + QUANTUS_NODE_DATA_PATH Node --base-path (default: $env:LOCALAPPDATA\quantus-node) + QUANTUS_COMPATIBILITY_URL Official compatibility manifest ($($script:CompatibilityUrl)) + +If sync stalls: + Check free disk space, peer connectivity and disk activity first. + Do not disable antivirus protection or add exclusions as a default fix. + Run status again to compare block progress before restarting. +"@ | Write-Output +} + +function Invoke-Setup { + Test-Platform + Test-SetupStorage + Ensure-Dirs + Info "Platform: windows / x64 ($($script:NodeTarget))" + Info "Working directory: $($script:MiningDir)" + + # -Force on an existing install refreshes binaries and the pinned pair only. + # The reward identity (preimage file, address, node name, resources) is kept, + # so moving an install forward, including from Planck to mainnet, never asks + # for the recovery phrase again or silently changes where rewards go. + $refresh = $false + if (Test-Path $script:ConfigFile) { + if ($Force -and (Test-Path $script:InnerHashFile) -and (Get-Item $script:InnerHashFile).Length -gt 0) { + $refresh = $true + Read-Config | Out-Null + } elseif (-not $Force) { + Warn "Config already exists at $($script:ConfigFile)" + $confirm = Read-Host 'Overwrite existing setup? (y/N)' + if ($confirm -notmatch '^(y|yes)$') { Info 'Setup cancelled.'; return } + } + } + + Install-Binaries + + if (-not (Test-Path $script:NodeKeyPath)) { + Info 'Generating node P2P identity...' + Invoke-Native { & $script:NodeBin key generate-node-key --file $script:NodeKeyPath 2>&1 | Out-Null } + } else { + Info "Using existing node key at $($script:NodeKeyPath)" + } + + if ($refresh) { + Info "Keeping node name: $($script:Config['NODE_NAME'])" + Info "Keeping reward address: $($script:Config['WORMHOLE_ADDRESS'])" + $script:Config['NODE_VERSION'] = $script:Manifest['_nodeVersion'] + $script:Config['MINER_VERSION'] = $script:Manifest['_minerVersion'] + $script:Config['MINER_PROTOCOL'] = $script:MinerProtocol + $script:Config['CHAIN'] = $script:Manifest['_chain'] + Write-Config + Save-InstallLocation + Copy-InstalledScript + Write-Output '' + Info "Binaries refreshed to node $($script:Manifest['_nodeVersion']) + miner $($script:Manifest['_minerVersion']) on $($script:Manifest['_chain']). Reward identity unchanged." + Info "Start mining with: $($script:RunHint) mine" + return + } + + $script:Config['NODE_NAME'] = Select-NodeName + Info "Node name: $($script:Config['NODE_NAME'])" + + New-WormholeKeys + Set-ResourceDefaults + $script:Config['NODE_VERSION'] = $script:Manifest['_nodeVersion'] + $script:Config['MINER_VERSION'] = $script:Manifest['_minerVersion'] + $script:Config['MINER_PROTOCOL'] = $script:MinerProtocol + $script:Config['CHAIN'] = $script:Manifest['_chain'] + Write-Config + Save-InstallLocation + Copy-InstalledScript + + if ([Environment]::UserInteractive -and -not [Console]::IsInputRedirected) { + Write-Output '' + $answer = Read-Host 'Start mining automatically when you sign in to Windows? (Y/n)' + if ($answer -notmatch '^(n|no)$') { Invoke-Autostart @('on') } + } + + Write-Output '' + Info 'Setup complete.' + Info "Start mining with: $($script:RunHint) mine" + Info 'Telemetry dashboard: https://telemetry.quantus.cat/' + Info 'If sync stalls, check free disk space and run status again to compare block progress.' +} + +# The parameter is not named $Args: that is PowerShell's automatic variable, +# it shadowed the argument, and every config subcommand printed usage. +function Invoke-Config([string[]]$ConfigArgs) { + $sub = if ($ConfigArgs.Count -gt 0) { $ConfigArgs[0] } else { '' } + switch ($sub) { + 'show' { + if (-not (Test-Path $script:ConfigFile)) { Fail "Config not found. Run: $($script:RunHint) setup" } + Get-Content $script:ConfigFile | Write-Output + } + 'set' { + if ($ConfigArgs.Count -lt 3) { Fail "Usage: $($script:RunHint) config set KEY VALUE" } + $key = $ConfigArgs[1]; $value = $ConfigArgs[2] + if ($script:EditableKeys -notcontains $key) { Fail "Key not editable via 'set': $key. Editable: $($script:EditableKeys -join ' ')" } + if (-not (Test-ConfigValue $key $value)) { Fail "Invalid $key. NODE_NAME: 3-32 lowercase letters, digits, hyphens. CPU_WORKERS, GPU_DEVICES: 0-256. MINER_LISTEN_PORT: 1024-65535." } + Read-Config | Out-Null + $script:Config[$key] = $value + Write-Config + Info "Updated $key=$value" + } + default { Fail "Usage: $($script:RunHint) config show|set KEY VALUE" } + } +} + +function Invoke-Start { + Confirm-StartPrerequisites + if (Test-StackRunning) { + Fail "Mining stack already running. Run: $($script:RunHint) stop" + } + $nodeLog = Join-Path $script:LogDir 'node.log' + $minerLog = Join-Path $script:LogDir 'miner.log' + + Info 'Starting quantus-node in background...' + $node = Start-Hidden $script:NodeBin (Get-NodeLaunchArgs) $nodeLog + Set-Content $script:NodePidFile $node.Id + Info "Node started (PID $($node.Id)). Log: $nodeLog" + + Wait-ForMinerServer ([int]$script:Config['MINER_LISTEN_PORT']) $nodeLog 120 + Wait-ForMinerAuthFiles 30 + + Info "Starting quantus-miner in background (log: $minerLog)..." + $miner = Start-Hidden $script:MinerBin (Get-MinerLaunchArgs) $minerLog + Start-Sleep 2 + if ($miner.HasExited) { + Warn "Last lines from ${minerLog}:" + Get-Content $minerLog -Tail 10 -ErrorAction SilentlyContinue | ForEach-Object { [Console]::Error.WriteLine($_) } + Fail "Miner exited immediately. Check $minerLog" + } + Set-Content $script:MinerPidFile $miner.Id + Info "Miner started (PID $($miner.Id))." + + Write-Output '' + Info 'Mining stack running in background.' + Info "Wait for full sync before expecting blocks (check $nodeLog or run: $($script:RunHint) status)." + Info "Telemetry: https://telemetry.quantus.cat/ (search for '$($script:Config['NODE_NAME'])')" + Info "Stop with: $($script:RunHint) stop" +} + +function Invoke-Stop { + $stopped = $false + if (Test-Path $script:ConfigFile) { Read-Config | Out-Null } + # Miner first, then node. + if (Stop-Tracked $script:MinerPidFile 'quantus-miner' 'quantus-miner') { $stopped = $true } + if (Stop-Tracked $script:NodePidFile 'quantus-node' 'quantus-node') { $stopped = $true } + if ($stopped) { Info 'Mining stack stopped.' } else { Warn "No running quantus-node or quantus-miner processes found under $($script:MiningDir)." } +} + +function Invoke-Status { + if (-not (Test-Path $script:ConfigFile)) { Fail "Mining is not configured. Run: $($script:RunHint) mine" } + $c = Read-Config + $nodeState = if (Test-ProcessAlive (Read-PidFile $script:NodePidFile) 'quantus-node') { 'Running' } else { 'Stopped' } + $minerState = if (Test-ProcessAlive (Read-PidFile $script:MinerPidFile) 'quantus-miner') { 'Running' } else { 'Stopped' } + + $syncState = 'Unknown'; $syncDetail = '' + $health = Invoke-NodeRpc 'system_health' + if ($null -ne $health) { + $peers = [int]$health.peers + $s1 = Invoke-NodeRpc 'system_syncState' + # isSyncing flips to false for a moment between import batches, so it is + # only believed when the block height agrees: Synced means within two + # blocks of the best height peers report. + $behind = ($null -ne $s1) -and ([long]$s1.highestBlock -gt 0) -and ([long]$s1.currentBlock -lt ([long]$s1.highestBlock - 2)) + $isSyncing = [bool]$health.isSyncing -or $behind + $syncState = if ($isSyncing) { 'Syncing' } else { 'Synced' } + if ($null -ne $s1 -and $isSyncing) { + $cur1 = [long]$s1.currentBlock; $high = [long]$s1.highestBlock + Start-Sleep 5 + $s2 = Invoke-NodeRpc 'system_syncState' + $cur2 = if ($null -ne $s2) { [long]$s2.currentBlock } else { $cur1 } + $rate = ($cur2 - $cur1) / 5.0 + $pct = if ($high -gt 0) { 100.0 * $cur2 / $high } else { 0 } + if ($rate -gt 0) { + $syncDetail = ('block {0:N0} of {1:N0} ({2:N1}%), {3:N0} blocks/s, about {4} left, {5} peers' -f $cur2, $high, $pct, $rate, (Format-Eta (($high - $cur2) / $rate)), $peers) + } elseif ($peers -gt 0) { + $syncDetail = ('block {0:N0} of {1:N0} ({2:N1}%), NOT ADVANCING with {3} peers' -f $cur2, $high, $pct, $peers) + $syncState = 'Stalled' + } else { + $syncDetail = ('block {0:N0} of {1:N0}, no peers yet' -f $cur2, $high) + } + } elseif (-not $isSyncing) { + $syncDetail = if ($null -ne $s1) { 'block {0:N0}, {1} peers' -f [long]$s1.currentBlock, $peers } else { "$peers peers" } + } + } + + $summary = Get-HashRateSummary (Join-Path $script:LogDir 'miner.log') + $hashRate = $summary.Text + $latest = if ($summary.Fresh) { $hashRate } else { '' } + + $overall = 'STARTING' + if ($nodeState -eq 'Running' -and $minerState -eq 'Running' -and $syncState -eq 'Synced' -and $latest) { $overall = 'MINING' } + elseif ($nodeState -eq 'Stopped' -or $minerState -eq 'Stopped') { $overall = 'STOPPED' } + + @" + +Quantus mining status +Overall: $overall +Network: $($c['CHAIN']) (mainnet QTC has value; keep your recovery phrase offline) +Compatibility: node $($c['NODE_VERSION']) + miner $($c['MINER_VERSION']) +Node: $nodeState +Sync: $syncState$(if ($syncDetail) { ", $syncDetail" }) +Miner: $minerState +Hash rate: $hashRate +Reward address: $($c['WORMHOLE_ADDRESS']) +Node name: $($c['NODE_NAME']) +Telemetry: https://telemetry.quantus.cat/ (search for $($c['NODE_NAME'])) +Restart recovery: Run $($script:RunHint) restart-check +"@ | Write-Output + + switch ($overall) { + 'MINING' { Info 'Success: the node is synced and the miner is reporting hash rate.' } + 'STOPPED' { Info "Recovery: run $($script:RunHint) mine to start the verified pair." } + default { + if ($syncState -eq 'Stalled') { + Info 'Recovery: check free disk space, peer connectivity and disk activity. The cause is not yet diagnosed.' + Info 'Do not disable antivirus protection or add exclusions as a default fix.' + Info "Compare progress with: $($script:RunHint) status" + } else { + Info "Recovery: wait for sync, then run $($script:RunHint) status again." + } + } + } +} + +function Invoke-Mine { + if (-not (Test-Path $script:ConfigFile)) { Invoke-Setup } + Read-Config | Out-Null + if (-not (Test-StackRunning)) { Invoke-Start } + Invoke-Status +} + +function Invoke-RestartCheck { + if (-not (Test-Path $script:ConfigFile)) { Fail "Mining is not configured. Run: $($script:RunHint) mine" } + Invoke-Stop + Invoke-Start + if ((Test-ProcessAlive (Read-PidFile $script:NodePidFile) 'quantus-node') -and (Test-ProcessAlive (Read-PidFile $script:MinerPidFile) 'quantus-miner')) { + Info 'Restart recovery: PASSED' + Invoke-Status + return + } + Fail "Restart recovery failed. Run $($script:RunHint) status, then apply the single recovery action shown." +} + +function Invoke-Uninstall { + $chainData = Get-NodeDataPath + if (-not (Test-Path $script:MiningDir)) { Warn "Nothing to uninstall at $($script:MiningDir)."; return } + if (-not $Force) { + Write-Output '' + Warn "This permanently removes $($script:MiningDir), including:" + Write-Output ' - mining.conf (public settings and reward address)' + Write-Output ' - rewards-inner-hash (owner-only reward preimage)' + Write-Output ' - node_key.p2p' + Write-Output ' - downloaded binaries and logs' + Write-Output '' + Warn 'Ensure your 24-word seed phrase is backed up before continuing.' + $confirm = Read-Host 'Uninstall Quantus mining setup? (y/N)' + if ($confirm -notmatch '^(y|yes)$') { Info 'Uninstall cancelled.'; return } + } + try { Invoke-Stop } catch { } + Info "Removing $($script:MiningDir)..." + if (Get-AutostartValue) { Remove-ItemProperty -Path $script:AutostartKey -Name $script:AutostartName } + if (Test-Path $script:LocationFile) { + $pointed = (Get-Content $script:LocationFile | Where-Object { $_ -like 'MINING_DIR=*' }) -replace '^MINING_DIR=', '' + if ($pointed -and ([IO.Path]::GetFullPath($pointed) -eq [IO.Path]::GetFullPath($script:MiningDir))) { Remove-Item $script:LocationFile -Force } + } + Remove-Item $script:MiningDir -Recurse -Force + Info 'Uninstall complete.' + if (Test-Path $chainData) { + Info "Chain sync data was not removed: $chainData" + Info 'Delete it manually to reclaim disk space.' + } +} + +# --------------------------------------------------------------------------- +# Main. Dot-source the file to load the functions without running a command. +# --------------------------------------------------------------------------- + +if ($MyInvocation.InvocationName -ne '.') { + switch ($Command) { + 'preflight' { Test-Platform; Test-SetupStorage } + 'mine' { Invoke-Mine } + 'setup' { Invoke-Setup } + 'config' { Invoke-Config $Rest } + 'start' { Invoke-Start } + 'stop' { Invoke-Stop } + 'restart' { Invoke-Stop; Invoke-Start } + 'restart-check' { Invoke-RestartCheck } + 'status' { Invoke-Status } + 'uninstall' { Invoke-Uninstall } + 'autostart' { Invoke-Autostart $Rest } + { $_ -in 'help', '-h', '--help' } { Invoke-Help } + default { Fail "Unknown command: $Command. Run: $($script:RunHint) help" } + } +} diff --git a/static/scripts/quantus-mining.ps1.sha256 b/static/scripts/quantus-mining.ps1.sha256 new file mode 100644 index 0000000..8bd072c --- /dev/null +++ b/static/scripts/quantus-mining.ps1.sha256 @@ -0,0 +1 @@ +f6edb313e7b4d01847f584736cd095776ac00fb8459d84893b566499f9b026db quantus-mining.ps1 diff --git a/static/scripts/quantus-mining.sh b/static/scripts/quantus-mining.sh index c0d5903..65bf3f1 100755 --- a/static/scripts/quantus-mining.sh +++ b/static/scripts/quantus-mining.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# quantus-mining.sh — Set up and manage Quantus mainnet mining. +# quantus-mining.sh - Set up and manage verified Quantus mainnet mining. # # Supports macOS, Linux, and WSL2. Requires bash, curl, and tar. # @@ -8,11 +8,13 @@ # Config file: ~/quantus-mining/mining.conf (mode 600) # # Usage: -# ./quantus-mining.sh setup [--force] # --force re-downloads binaries and keeps INNER_HASH +# ./quantus-mining.sh mine +# ./quantus-mining.sh setup [--force] # ./quantus-mining.sh config show|set KEY VALUE|edit # ./quantus-mining.sh start [-d|--detach] # ./quantus-mining.sh start-node|start-miner # ./quantus-mining.sh stop|restart [-d|--detach] +# ./quantus-mining.sh status|restart-check # ./quantus-mining.sh uninstall [--force] # ./quantus-mining.sh help # @@ -21,6 +23,23 @@ set -euo pipefail readonly SCRIPT_NAME="$(basename "$0")" + +# Where the last setup installed to, so later commands find it without the +# user re-exporting QUANTUS_MINING_DIR / QUANTUS_NODE_DATA_PATH. Environment +# variables still win. Parsed line by line, never sourced. +readonly LOCATION_FILE="${XDG_CONFIG_HOME:-$HOME/.config}/quantus-mining/location" +if [ -z "${QUANTUS_MINING_DIR:-}" ] && [ -f "$LOCATION_FILE" ]; then + while IFS='=' read -r location_key location_value; do + case "$location_value" in /*) ;; *) continue ;; esac + case "$location_value" in *[\'\"\$\`]*) continue ;; esac + case "$location_key" in + MINING_DIR) QUANTUS_MINING_DIR="$location_value" ;; + NODE_DATA_PATH) [ -n "${QUANTUS_NODE_DATA_PATH:-}" ] || QUANTUS_NODE_DATA_PATH="$location_value" ;; + esac + done < "$LOCATION_FILE" + export QUANTUS_MINING_DIR + [ -z "${QUANTUS_NODE_DATA_PATH:-}" ] || export QUANTUS_NODE_DATA_PATH +fi readonly DEFAULT_MINING_DIR="${HOME}/quantus-mining" readonly MINING_DIR="${QUANTUS_MINING_DIR:-$DEFAULT_MINING_DIR}" readonly CONFIG_FILE="${MINING_DIR}/mining.conf" @@ -31,15 +50,23 @@ readonly MINER_BIN="${BIN_DIR}/quantus-miner" readonly NODE_PID_FILE="${MINING_DIR}/node.pid" readonly MINER_PID_FILE="${MINING_DIR}/miner.pid" readonly NODE_KEY_PATH="${MINING_DIR}/node_key.p2p" +readonly INNER_HASH_FILE="${MINING_DIR}/rewards-inner-hash" +readonly COMPATIBILITY_FILE="${MINING_DIR}/mining-compatibility.json" +readonly COMPATIBILITY_URL="${QUANTUS_COMPATIBILITY_URL:-https://docs.quantus.com/mining-compatibility.json}" readonly CHAIN_REPO="Quantus-Network/chain" readonly MINER_REPO="Quantus-Network/quantus-miner" -readonly EDITABLE_KEYS="NODE_NAME CPU_WORKERS GPU_DEVICES MINER_LISTEN_PORT CHAIN NODE_VERSION MINER_VERSION" +readonly EDITABLE_KEYS="NODE_NAME CPU_WORKERS GPU_DEVICES MINER_LISTEN_PORT GPU_THROTTLE_MS" OS="" ARCH="" +PLATFORM_KEY="" NODE_TARGET="" MINER_ASSET="" +NODE_DOWNLOAD_URL="" +NODE_DOWNLOAD_SHA256="" +MINER_DOWNLOAD_URL="" +MINER_DOWNLOAD_SHA256="" DOCKER_COMPOSE="" NODE_LAUNCH_ARGS=() @@ -70,7 +97,7 @@ detect_platform() { Linux*) OS="linux" ;; Darwin*) OS="macos" ;; *) - die "Unsupported operating system: $(uname -s). Use macOS, Linux, or WSL2." + die "Unsupported operating system: $(uname -s). Use macOS, Linux, or WSL2. On native Windows, use quantus-mining.ps1 in PowerShell." ;; esac @@ -78,9 +105,11 @@ detect_platform() { x86_64|amd64) ARCH="x86_64" if [ "$OS" = "linux" ]; then + PLATFORM_KEY="LinuxX8664" NODE_TARGET="x86_64-unknown-linux-gnu" MINER_ASSET="quantus-miner-linux-x86_64" else + PLATFORM_KEY="DarwinX8664" NODE_TARGET="x86_64-apple-darwin" MINER_ASSET="quantus-miner-macos-x86_64" fi @@ -88,9 +117,11 @@ detect_platform() { arm64|aarch64) ARCH="arm64" if [ "$OS" = "linux" ]; then + PLATFORM_KEY="LinuxArm64" NODE_TARGET="aarch64-unknown-linux-gnu" MINER_ASSET="" else + PLATFORM_KEY="DarwinArm64" NODE_TARGET="aarch64-apple-darwin" MINER_ASSET="quantus-miner-macos-aarch64" fi @@ -260,21 +291,118 @@ tolower() { printf '%s' "$1" | tr '[:upper:]' '[:lower:]' } -fetch_latest_tag() { - local repo="$1" - local release_json tag +manifest_string() { + local file="$1" key="$2" + sed -n "s/^[[:space:]]*\"${key}\"[[:space:]]*:[[:space:]]*\"\([^\"]*\)\"[,[:space:]]*$/\1/p" "$file" \ + | head -n 1 +} + +require_manifest_string() { + local file="$1" key="$2" value + value="$(manifest_string "$file" "$key")" + [ -n "$value" ] || die "Compatibility manifest is missing ${key}. No files were installed." + printf '%s' "$value" +} + +sha256_file() { + local file="$1" + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$file" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$file" | awk '{print $1}' + else + die "No SHA-256 tool found. Install sha256sum (Linux) or use macOS shasum, then retry." + fi +} + +verify_sha256() { + local file="$1" expected="$2" actual + case "$expected" in + *[!0-9a-f]*|'') die "Invalid SHA-256 value for $(basename "$file"). No files were installed." ;; + esac + [ "${#expected}" -eq 64 ] \ + || die "Invalid SHA-256 length for $(basename "$file"). No files were installed." + + actual="$(sha256_file "$file")" + if [ "$actual" != "$expected" ]; then + die "Checksum verification failed for $(basename "$file"). +Expected: ${expected} +Actual: ${actual} +The file was not installed. Delete the download and retry." + fi + info "Verified SHA-256: $(basename "$file")" +} + +validate_release_url() { + local url="$1" repo="$2" version="$3" + case "$url" in + "https://github.com/${repo}/releases/download/${version}/"*) ;; + *) die "Compatibility manifest contains an unexpected download URL for ${repo}. No files were installed." ;; + esac +} + +load_compatibility_manifest() { + local file="$1" status network_kind token_value evidence + local node_url_key node_sha_key miner_url_key miner_sha_key + + [ -s "$file" ] || die "Compatibility manifest not found at ${file}. Run: ${SCRIPT_NAME} setup --force" + + status="$(require_manifest_string "$file" status)" + [ "$status" = "supported" ] \ + || die "Mining compatibility status is '${status}', not 'supported'. Nothing will be installed or started." + + CHAIN="$(require_manifest_string "$file" networkId)" + network_kind="$(require_manifest_string "$file" networkKind)" + token_value="$(require_manifest_string "$file" tokenValue)" + NODE_VERSION="$(require_manifest_string "$file" nodeVersion)" + MINER_VERSION="$(require_manifest_string "$file" minerVersion)" + MINER_PROTOCOL="$(require_manifest_string "$file" minerProtocol)" + evidence="$(require_manifest_string "$file" compatibilityEvidenceUrl)" + + [ "$CHAIN" = "mainnet" ] && [ "$network_kind" = "mainnet" ] && [ -n "$token_value" ] \ + || die "This installer is restricted to Quantus mainnet. The manifest requested a different network." + [ "$MINER_PROTOCOL" = "quantus-miner/2" ] \ + || die "Unsupported miner protocol '${MINER_PROTOCOL}'. Nothing will be installed or started." + case "$evidence" in + https://github.com/Quantus-Network/quantus-miner/releases/tag/*) ;; + *) die "Compatibility evidence URL is not an official Quantus miner release." ;; + esac - release_json="$(curl -fsSL "https://api.github.com/repos/${repo}/releases/latest")" \ - || die "Failed to fetch latest release for ${repo}" + node_url_key="node${PLATFORM_KEY}Url" + [ -n "$(manifest_string "$file" "$node_url_key")" ] \ + || die "The supported release pair (node ${NODE_VERSION}) has no build for ${OS}/${ARCH}. Nothing was installed. Use Apple Silicon macOS, Linux x64, or Windows x64." + node_sha_key="node${PLATFORM_KEY}Sha256" + miner_url_key="miner${PLATFORM_KEY}Url" + miner_sha_key="miner${PLATFORM_KEY}Sha256" + NODE_DOWNLOAD_URL="$(require_manifest_string "$file" "$node_url_key")" + NODE_DOWNLOAD_SHA256="$(require_manifest_string "$file" "$node_sha_key")" + MINER_DOWNLOAD_URL="$(require_manifest_string "$file" "$miner_url_key")" + MINER_DOWNLOAD_SHA256="$(require_manifest_string "$file" "$miner_sha_key")" + + validate_release_url "$NODE_DOWNLOAD_URL" "$CHAIN_REPO" "$NODE_VERSION" + validate_release_url "$MINER_DOWNLOAD_URL" "$MINER_REPO" "$MINER_VERSION" + case "$(basename "$NODE_DOWNLOAD_URL")" in + "quantus-node-${NODE_VERSION}-${NODE_TARGET}.tar.gz") ;; + *) die "Node asset does not match ${OS}/${ARCH}. No files were installed." ;; + esac + [ "$(basename "$MINER_DOWNLOAD_URL")" = "$MINER_ASSET" ] \ + || die "Miner asset does not match ${OS}/${ARCH}. No files were installed." +} - tag="$(printf '%s' "$release_json" | grep -o '"tag_name": "[^"]*"' | head -n 1 | cut -d'"' -f4)" - [ -n "$tag" ] || die "Could not determine latest release tag for ${repo}" - printf '%s' "$tag" +fetch_compatibility_manifest() { + local temp_file="${COMPATIBILITY_FILE}.download" + info "Fetching the supported mainnet release pair..." + curl --proto '=https' --tlsv1.2 -fsSL "$COMPATIBILITY_URL" -o "$temp_file" \ + || die "Could not download ${COMPATIBILITY_URL}. Check your connection and retry." + load_compatibility_manifest "$temp_file" + mv "$temp_file" "$COMPATIBILITY_FILE" + chmod 600 "$COMPATIBILITY_FILE" + info "Supported pair: node ${NODE_VERSION} + miner ${MINER_VERSION} (${MINER_PROTOCOL})" } # Miner protocol this script can drive: # auth = Ready { token } + TLS pin (quantus-miner/2) -# legacy = unauthenticated Ready (pre-auth releases, e.g. node v0.9.0 / miner v3.3.1) +# legacy = unauthenticated Ready from older release pairs MINER_PROTOCOL="" incompatible_miner_pair_die() { @@ -284,14 +412,8 @@ incompatible_miner_pair_die() { Miner --auth-token-file / --tls-cert-sha256-file: ${miner_auth} Do not mix a release that requires Ready { token } (quantus-miner/2) with one that does not. -GitHub latest tags are published independently — pin a matching pair: - - NODE_VERSION= - MINER_VERSION= - -in ${CONFIG_FILE}, or re-run setup after coordinated releases: - https://github.com/${CHAIN_REPO}/releases - https://github.com/${MINER_REPO}/releases" +Run ${SCRIPT_NAME} setup --force to reinstall the supported pair from: + ${COMPATIBILITY_URL}" } classify_miner_protocol() { @@ -342,18 +464,22 @@ maybe_wait_for_miner_auth_files() { } download_node_binary() { - local tag="$1" - local asset="quantus-node-${tag}-${NODE_TARGET}.tar.gz" - local url="https://github.com/${CHAIN_REPO}/releases/download/${tag}/${asset}" + local tag="$1" url="$2" expected_sha256="$3" + local asset local temp_dir asset_path + asset="$(basename "$url")" + info "Downloading quantus-node ${tag} for ${NODE_TARGET}..." temp_dir="$(mktemp -d)" asset_path="${temp_dir}/${asset}" - curl -fsSL "$url" -o "$asset_path" || { rm -rf "$temp_dir"; die "Failed to download ${url}"; } + curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$asset_path" \ + || { rm -rf "$temp_dir"; die "Failed to download ${url}"; } + verify_sha256 "$asset_path" "$expected_sha256" - tar -xzf "$asset_path" -C "$temp_dir" + tar -xzf "$asset_path" -C "$temp_dir" \ + || { rm -rf "$temp_dir"; die "Could not extract ${asset}. Delete the download and retry."; } if [ ! -f "${temp_dir}/quantus-node" ]; then rm -rf "$temp_dir" die "quantus-node not found in archive" @@ -366,162 +492,81 @@ download_node_binary() { } download_miner_binary() { - local tag="$1" - local url="https://github.com/${MINER_REPO}/releases/download/${tag}/${MINER_ASSET}" + local tag="$1" url="$2" expected_sha256="$3" + local temp_file info "Downloading quantus-miner ${tag} (${MINER_ASSET})..." - curl -fsSL "$url" -o "$MINER_BIN" || die "Failed to download ${url}" + temp_file="${MINER_BIN}.download" + curl --proto '=https' --tlsv1.2 -fsSL "$url" -o "$temp_file" \ + || die "Failed to download ${url}" + verify_sha256 "$temp_file" "$expected_sha256" + mv "$temp_file" "$MINER_BIN" make_executable "$MINER_BIN" info "Installed quantus-miner to ${MINER_BIN}" } -read_conf_value() { - local key="$1" - local raw="" - [ -f "$CONFIG_FILE" ] || return 0 - raw="$(grep -E "^${key}=" "$CONFIG_FILE" 2>/dev/null | head -n 1 | cut -d= -f2- || true)" - raw="${raw#\"}" - raw="${raw%\"}" - printf '%s' "$raw" -} - -# Env NODE_VERSION / MINER_VERSION override mining.conf. Empty pins fetch GitHub latest. -load_version_pins() { - local conf_node conf_miner - conf_node="$(read_conf_value NODE_VERSION)" - conf_miner="$(read_conf_value MINER_VERSION)" - if [ -z "${NODE_VERSION:-}" ] && [ -n "$conf_node" ]; then - NODE_VERSION="$conf_node" - fi - if [ -z "${MINER_VERSION:-}" ] && [ -n "$conf_miner" ]; then - MINER_VERSION="$conf_miner" - fi -} - download_binaries() { - local force="${1:-false}" - local node_tag miner_tag - - load_version_pins - - if [ "$force" = "true" ] || [ ! -x "$NODE_BIN" ]; then - node_tag="${NODE_VERSION:-}" - [ -n "$node_tag" ] || node_tag="$(fetch_latest_tag "$CHAIN_REPO")" - download_node_binary "$node_tag" - else - node_tag="${NODE_VERSION:-}" - [ -n "$node_tag" ] || node_tag="$(fetch_latest_tag "$CHAIN_REPO")" - info "Using existing quantus-node at ${NODE_BIN}" - fi - - if [ "$force" = "true" ] || [ ! -x "$MINER_BIN" ]; then - miner_tag="${MINER_VERSION:-}" - [ -n "$miner_tag" ] || miner_tag="$(fetch_latest_tag "$MINER_REPO")" - download_miner_binary "$miner_tag" - else - miner_tag="${MINER_VERSION:-}" - [ -n "$miner_tag" ] || miner_tag="$(fetch_latest_tag "$MINER_REPO")" - info "Using existing quantus-miner at ${MINER_BIN}" - fi - - NODE_VERSION="$node_tag" - MINER_VERSION="$miner_tag" + fetch_compatibility_manifest + download_node_binary "$NODE_VERSION" "$NODE_DOWNLOAD_URL" "$NODE_DOWNLOAD_SHA256" + download_miner_binary "$MINER_VERSION" "$MINER_DOWNLOAD_URL" "$MINER_DOWNLOAD_SHA256" detect_binary_miner_protocol + [ "$MINER_PROTOCOL" = "auth" ] \ + || die "The downloaded binaries do not implement the manifest protocol quantus-miner/2. Nothing will be started." } parse_wormhole_output() { local output="$1" - local line WORMHOLE_ADDRESS="$(printf '%s\n' "$output" | grep -E '^Address:' | head -n 1 | awk '{print $2}')" INNER_HASH="$(printf '%s\n' "$output" | grep -E '^Inner [Hh]ash:' | head -n 1 | awk '{print $3}')" if [ -z "$INNER_HASH" ]; then INNER_HASH="$(printf '%s\n' "$output" | grep -E '^inner_hash:' | head -n 1 | awk '{print $2}')" fi - WORMHOLE_SECRET="" - line="$(printf '%s\n' "$output" | grep -E '^Secret:' | head -n 1 || true)" - if [ -n "$line" ]; then - WORMHOLE_SECRET="$(printf '%s' "$line" | awk '{print $2}')" - fi - - line="$(printf '%s\n' "$output" | grep -E '^Secret phrase:' | head -n 1 || true)" - if [ -n "$line" ]; then - WORMHOLE_SECRET_PHRASE="${line#*Secret phrase: }" - else - WORMHOLE_SECRET_PHRASE="" - fi - [ -n "$WORMHOLE_ADDRESS" ] || die "Could not parse wormhole Address from keygen output" [ -n "$INNER_HASH" ] || die "Could not parse Inner Hash from keygen output" } generate_wormhole_keys() { - local choice output mnemonic + local output mnemonic echo "" - echo "Wormhole address generation:" - echo " [1] Derive from existing 24-word wallet mnemonic (recommended)" - echo " [2] Generate a fresh keypair" - read -r -p "Enter choice (1/2) [1]: " choice - choice="${choice:-1}" - - case "$choice" in - 1) - echo "Enter your 24-word mnemonic (input hidden):" - read -r -s mnemonic - echo "" - [ -n "$mnemonic" ] || die "Mnemonic cannot be empty" - output="$(printf '%s\n' "$mnemonic" | wormhole_keygen --words)" - ;; - 2) - output="$(wormhole_keygen)" - ;; - *) - die "Invalid choice: $choice" - ;; - esac + info "Wallet step: enter your existing Quantus 24-word recovery phrase locally." + info "Input is hidden and is not written to disk, logs, command arguments, or network requests." + info "Never paste a recovery phrase into chat or a support ticket." + read -r -s -p "Recovery phrase: " mnemonic + echo "" + [ -n "$mnemonic" ] || die "Recovery phrase cannot be empty. Open your Quantus wallet backup and retry." + output="$(printf '%s\n' "$mnemonic" | wormhole_keygen --words)" + unset mnemonic parse_wormhole_output "$output" + unset output + + printf '%s\n' "$INNER_HASH" > "$INNER_HASH_FILE" + chmod 600 "$INNER_HASH_FILE" echo "" - echo "Wormhole keypair generated. Save these values securely:" - echo "$output" - echo "" - warn "Back up your 24-word seed phrase. Loss means loss of mining rewards." + info "Reward address: ${WORMHOLE_ADDRESS}" + info "Your recovery phrase was not saved. Keep your existing offline backup." } -prompt_resource_allocation() { - local cores has_gpu choice default_workers +configure_resource_defaults() { + local cores default_workers cores="$(cpu_count)" - echo "" - echo "This machine has ${cores} CPU cores." - echo "GPU mining is strongly recommended (much faster than the built-in CPU loop)." - read -r -p "Do you have a GPU available for mining? (y/N): " has_gpu - - case "$(tolower "$has_gpu")" in - y|yes) - GPU_DEVICES=1 - CPU_WORKERS=0 - info "Default: GPU mining with --gpu-devices 1 --cpu-workers 0" - ;; - *) - default_workers=$((cores - 2)) - [ "$default_workers" -lt 1 ] && default_workers=1 - GPU_DEVICES=0 - CPU_WORKERS="$default_workers" - info "Default: CPU-only mining with --cpu-workers ${CPU_WORKERS} (leaving 2 cores for OS/node)" - ;; - esac - - read -r -p "CPU workers [${CPU_WORKERS}]: " choice - if [ -n "$choice" ]; then - CPU_WORKERS="$choice" - fi - - read -r -p "GPU devices [${GPU_DEVICES}]: " choice - if [ -n "$choice" ]; then - GPU_DEVICES="$choice" + default_workers=$((cores - 2)) + [ "$default_workers" -lt 1 ] && default_workers=1 + + if [ "$OS" = "macos" ] \ + || command -v nvidia-smi >/dev/null 2>&1 \ + || command -v vulkaninfo >/dev/null 2>&1; then + GPU_DEVICES=1 + CPU_WORKERS=0 + info "Mining resources: one detected GPU, CPU reserved for the node." + else + GPU_DEVICES=0 + CPU_WORKERS="$default_workers" + info "Mining resources: ${CPU_WORKERS} CPU workers, two cores reserved when available." fi } @@ -530,20 +575,21 @@ write_config() { MINER_LISTEN_PORT="${MINER_LISTEN_PORT:-9833}" CPU_WORKERS="${CPU_WORKERS:-0}" GPU_DEVICES="${GPU_DEVICES:-0}" + GPU_THROTTLE_MS="${GPU_THROTTLE_MS:-0}" cat > "$CONFIG_FILE" < "$LOCATION_FILE" +} + +# Turns the miner's log into one plain number. Each GPU worker logs every search +# as "GPU worker N ...: H hashes in T s". A single search can be two seconds +# long when a new block cuts it short, so one line swings widely. The rate shown +# is total hashes over total seconds per worker in the last three minutes, +# summed across workers. Nothing in that window means the miner is not working +# now, even if its process is still alive. +hash_rate_summary() { + local log="$1" cutoff + HASH_RATE_TEXT="Waiting for miner output" + HASH_RATE_FRESH="false" + [ -f "$log" ] || return 0 + cutoff="$(date -u -d '-3 minutes' +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -u -v-3M +%Y-%m-%dT%H:%M:%S)" + HASH_RATE_TEXT="$(tail -n 2000 "$log" | awk -v cutoff="$cutoff" ' + match($0, /^\[[0-9-]+T[0-9:]+Z/) { + ts = substr($0, 2, 19) + if (!match($0, /GPU worker [0-9]+/)) next + split(substr($0, RSTART, RLENGTH), wparts, " "); w = wparts[3] + if (!match($0, /[0-9]+ hashes in [0-9.]+s/)) next + split(substr($0, RSTART, RLENGTH), hparts, " ") + last = ts + if (ts < cutoff) next + h[w] += hparts[1]; sub(/s$/, "", hparts[4]); t[w] += hparts[4]; seen = 1 + } + END { + total = 0 + for (k in h) if (t[k] > 0) total += h[k] / t[k] + if (total >= 1e9) printf "%.1f GH/s", total / 1e9 + else if (total >= 1e6) printf "%.1f MH/s", total / 1e6 + else if (total >= 1e3) printf "%.1f KH/s", total / 1e3 + else if (total > 0) printf "%d H/s", total + else if (last != "") printf "STALE" + }')" + case "$HASH_RATE_TEXT" in + "") HASH_RATE_TEXT="Waiting for miner output" ;; + STALE) HASH_RATE_TEXT="no hashes in the last 3 min, miner is not working now" ;; + *) HASH_RATE_FRESH="true" ;; + esac +} + +valid_node_name() { + case "$1" in + ''|*[!a-z0-9-]*|-*|*-) return 1 ;; + esac + [ "${#1}" -ge 3 ] && [ "${#1}" -le 32 ] +} + +valid_config_value() { + local key="$1" value="$2" + case "$key" in + NODE_NAME) valid_node_name "$value" ;; + GPU_THROTTLE_MS) + case "$value" in ''|*[!0-9]*) return 1 ;; esac + [ "$value" -le 1000 ] + ;; + CPU_WORKERS|GPU_DEVICES) + case "$value" in ''|*[!0-9]*) return 1 ;; esac + [ "$value" -le 256 ] + ;; + MINER_LISTEN_PORT) + case "$value" in ''|*[!0-9]*) return 1 ;; esac + [ "$value" -ge 1024 ] && [ "$value" -le 65535 ] + ;; + *) return 1 ;; + esac +} + +default_node_name() { + local host + host="$(hostname 2>/dev/null | tr '[:upper:]' '[:lower:]' | tr -cd 'a-z0-9-' | sed 's/^-*//; s/-*$//' | cut -c1-24)" + if [ -n "$host" ] && valid_node_name "quantus-${host}"; then + printf 'quantus-%s' "$host" + else + printf 'quantus-miner' + fi +} + +# Asks for the public telemetry name. QUANTUS_NODE_NAME answers it without a +# prompt, and when stdin is not a terminal (an agent, CI) the default is used +# instead of blocking on input nobody can give. +choose_node_name() { + local default answer + default="$(default_node_name)" + if [ -n "${QUANTUS_NODE_NAME:-}" ]; then + valid_node_name "$QUANTUS_NODE_NAME" \ + || die "QUANTUS_NODE_NAME must be 3-32 lowercase letters, digits and hyphens." + NODE_NAME="$QUANTUS_NODE_NAME" + return + fi + if [ ! -t 0 ]; then + NODE_NAME="$default" + return + fi + echo "" + info "Your miner's name is shown publicly on https://telemetry.quantus.cat/." + info "Use lowercase letters, numbers and hyphens. Avoid your real name." + while true; do + read -r -p "Miner name [${default}]: " answer + answer="$(printf '%s' "${answer:-$default}" | tr '[:upper:]' '[:lower:]')" + if valid_node_name "$answer"; then + NODE_NAME="$answer" + return + fi + warn "Use 3-32 lowercase letters, numbers and hyphens, not starting or ending with a hyphen." + done +} + validate_editable_key() { local key="$1" case " ${EDITABLE_KEYS} " in @@ -880,14 +1037,15 @@ validate_editable_key() { cmd_help() { cat </dev/null)"; then + case "$health" in + *'"isSyncing":false'*) sync_state="Synced" ;; + *'"isSyncing":true'*) sync_state="Syncing" ;; + esac + # isSyncing flips to false for a moment between import batches, so it is + # only believed when the block height agrees: Synced means within two + # blocks of the best height peers report. + if sync_json="$(curl --max-time 2 -fsS -H 'Content-Type: application/json' \ + -d '{"jsonrpc":"2.0","id":1,"method":"system_syncState","params":[]}' \ + http://127.0.0.1:9944 2>/dev/null)"; then + current_block="$(printf '%s' "$sync_json" | sed -n 's/.*"currentBlock":\([0-9]*\).*/\1/p')" + highest_block="$(printf '%s' "$sync_json" | sed -n 's/.*"highestBlock":\([0-9]*\).*/\1/p')" + if [ -n "$current_block" ] && [ -n "$highest_block" ]; then + if [ "$current_block" -lt $((highest_block - 2)) ]; then + sync_state="Syncing" + fi + sync_state="${sync_state}, block ${current_block} of ${highest_block}" + fi + fi + fi + + hash_rate_summary "${LOG_DIR}/miner.log" + hash_rate="$HASH_RATE_TEXT" + latest_rate="" + [ "$HASH_RATE_FRESH" = "true" ] && latest_rate="$HASH_RATE_TEXT" + + if [ "$node_state" = "Running" ] && [ "$miner_state" = "Running" ] \ + && case "$sync_state" in Synced*) true ;; *) false ;; esac && [ -n "$latest_rate" ]; then + overall="MINING" + elif [ "$node_state" = "Stopped" ] || [ "$miner_state" = "Stopped" ]; then + overall="STOPPED" + fi + + cat <