diff --git a/config.reference.json b/config.reference.json index 148a883..8effff3 100644 --- a/config.reference.json +++ b/config.reference.json @@ -1,8 +1,22 @@ { - "_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. A pool's 'socks5' (default null) dials that pool through a SOCKS5 proxy given as host:port \u2014 e.g. \"127.0.0.1:9050\" for a local Tor client, which is how a rig reaches a stratum published as an onion service; XMRig sends the hostname to the proxy, so the proxy resolves it. RigForge points the miner at a proxy, it does not install or run one. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'ACCESS_TOKENS' (default {}) is an optional name -> token map, one token per Pithead stack that consumes this rig, so rotating the bench's token never touches production's: every entry is accepted on the sister API (:8081, raw or its own HMAC-derived read bearer) and the writable control path (:8082, raw), while XMRig's own :8080 and the control path's pools restore stay ACCESS_TOKEN's alone — a stack that probes :8080 directly must hold the master. 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on main (the release branch), throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'hugepages_pool_ceiling_mb' (default 0 = no ceiling) is a HARD CAP on the runtime pool RigForge will grow to, in MB, distinct from hugepages_reserve_extra_mb (which adds declared headroom into the computed requirement rather than bounding the write) — set it on a RAM-constrained co-resident box so the grow-only write can never exceed the box's honest capacity no matter what the requirement/availability math computes (rigforge#398; an odd MB value floors to the 2MB page below, e.g. 5121 -> 5120MB effective, never rounds up past the declared ceiling). It bounds the PERSISTED reservation too: the 2MB `hugepages=` RigForge merges into the GRUB cmdline is capped the same way, so the pool does not come back up over the ceiling after a reboot, where the grow-only runtime write could never bring it down again (rigforge#410). The 1GB dataset reservation is a separate pool this key has never described and is not capped, so a ceiling below a box's 1GB reservation still leaves it holding more hugepage memory than the ceiling names; 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference Optional solo daemon pool example (replace an entry in pools): {\"url\": \"daemon+http://\", \"user\": \"\", \"daemon\": true, \"daemon-poll-interval\": 1000, \"daemon-job-timeout\": 15000}. Optional example: replace NODE_AUTHORITY with your node's host and RPC port, and PAYOUT_USER with its payout identity. RigForge does not install, run or supervise the node. self-select, submit-to-origin and spend-secret-key are unsupported. A daemon+ URL implies daemon: true; explicit false is refused. Timing values are positive JSON integers no greater than 9007199254740991 milliseconds; integral decimal and exponent values are written in integer notation. Omit optional fields to use XMRig defaults. Daemon pools cannot use socks5 or tls-fingerprint because the pinned miner ignores them. Daemon HTTPS encrypts without authenticating the peer; doctor's HTTPS RPC check uses curl's CA verification.", + "_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. Optional pool fields: 'rig-id' is a separate worker label while 'user' remains the login; 'nicehash' enables the pool-assigned nonce protocol; 'algo' pins a supported rx/* algorithm; 'coin' selects a supported RandomX coin by case-insensitive name or code; and 'sni' sends the pool hostname during TLS setup. These five fields are omitted when absent or null; explicit false is preserved for the strict-boolean 'nicehash' and 'sni' fields. 'pass' is the Stratum password or worker name; 'keepalive' sends requests that prevent idle timeout; 'tls' enables encrypted Stratum; and 'enabled' controls whether XMRig may connect to the entry during failover. A pool's 'socks5' (default null) dials that pool through a SOCKS5 proxy given as host:port \u2014 e.g. \"127.0.0.1:9050\" for a local Tor client, which is how a rig reaches a stratum published as an onion service; XMRig sends the hostname to the proxy, so the proxy resolves it. RigForge points the miner at a proxy, it does not install or run one. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'ACCESS_TOKENS' (default {}) is an optional name -> token map, one token per Pithead stack that consumes this rig, so rotating the bench's token never touches production's: every entry is accepted on the sister API (:8081, raw or its own HMAC-derived read bearer) and the writable control path (:8082, raw), while XMRig's own :8080 and the control path's pools restore stay ACCESS_TOKEN's alone — a stack that probes :8080 directly must hold the master. 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on main (the release branch), throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'hugepages_pool_ceiling_mb' (default 0 = no ceiling) is a HARD CAP on the runtime pool RigForge will grow to, in MB, distinct from hugepages_reserve_extra_mb (which adds declared headroom into the computed requirement rather than bounding the write) — set it on a RAM-constrained co-resident box so the grow-only write can never exceed the box's honest capacity no matter what the requirement/availability math computes (rigforge#398; an odd MB value floors to the 2MB page below, e.g. 5121 -> 5120MB effective, never rounds up past the declared ceiling). It bounds the PERSISTED reservation too: the 2MB `hugepages=` RigForge merges into the GRUB cmdline is capped the same way, so the pool does not come back up over the ceiling after a reboot, where the grow-only runtime write could never bring it down again (rigforge#410). The 1GB dataset reservation is a separate pool this key has never described and is not capped, so a ceiling below a box's 1GB reservation still leaves it holding more hugepage memory than the ceiling names; 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. The pool 'coin' lets XMRig choose the algorithm for that coin. Both 'algo' and 'coin' defer to pool algorithm negotiation; avoid 'coin' with NiceHash, where the pool controls the coin. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference Optional solo daemon pool example (replace an entry in pools): {\"url\": \"daemon+http://\", \"user\": \"\", \"daemon\": true, \"daemon-poll-interval\": 1000, \"daemon-job-timeout\": 15000}. Optional example: replace NODE_AUTHORITY with your node's host and RPC port, and PAYOUT_USER with its payout identity. RigForge does not install, run or supervise the node. self-select, submit-to-origin and spend-secret-key are unsupported. A daemon+ URL implies daemon: true; explicit false is refused. Timing values are positive JSON integers no greater than 9007199254740991 milliseconds; integral decimal and exponent values are written in integer notation. Omit optional fields to use XMRig defaults. Daemon pools cannot use socks5 or tls-fingerprint because the pinned miner ignores them. Daemon HTTPS encrypts without authenticating the peer; doctor's HTTPS RPC check uses curl's CA verification.", "pools": [ - { "url": ":3333", "user": "", "pass": "x", "keepalive": true, "tls": false, "tls-fingerprint": null, "socks5": null, "enabled": true } + { + "url": ":3333", + "user": "", + "pass": "x", + "rig-id": null, + "nicehash": false, + "algo": null, + "coin": null, + "keepalive": true, + "tls": false, + "tls-fingerprint": null, + "sni": false, + "socks5": null, + "enabled": true + } ], "ACCESS_TOKEN": "", diff --git a/docs/configuration.md b/docs/configuration.md index c0073a1..341d587 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -85,10 +85,10 @@ You don't write XMRig's config. RigForge generates it in-script and writes it in the live `config.json` the service runs from. There's no template file to keep in sync and no config key for it. Every run (re-runs included) rebuilds the config from four sources: -1. Your `config.json`: the `pools` array (with `user`/`pass`/`keepalive`/`tls` and failover - defaults filled in), the `donate-level`, and the `http` API block (bound to the LAN, read-only, - open by default; set `ACCESS_TOKEN` for a bearer token). These are the keys documented in the - [reference table](#configuration-reference). +1. Your `config.json`: the `pools` array (with connection defaults filled in and supported optional + fields preserved), the `donate-level`, and the `http` API block (bound to the LAN, read-only, open + by default; set `ACCESS_TOKEN` for a bearer token). These are the keys documented in the [reference + table](#configuration-reference). 2. Detected hardware: the per-CPU `cpu`/`randomx` tuning (thread count, `asm`, MSR, NUMA, HugePages). RigForge uses XMRig's own cache-aware auto-detection rather than a CPU-model table, so it stays correct for CPUs it's never seen. See [Hardware Requirements](hardware.md). @@ -127,21 +127,38 @@ The pool target uses XMRig's native `pools` array with the supported fields belo Only `url` is required; every other field has a default, so you specify only what you care about: -| Field | Default if blank/omitted | +| Field | Default and meaning | |---|---| | `url` | *(required)* — `host:port` (e.g. `pool.supportxmr.com:443` or `your-stack:3333`). For an IPv6 literal, use the bracketed `[2001:db8::1]:3333` form. | -| `user` | the machine hostname. For Pithead this is the rig's dashboard label; for a public pool set it to your Monero wallet address (see below). | +| `user` | the machine hostname. This remains the pool login when `rig-id` is set. For Pithead this is the rig's dashboard label; for a public pool set it to your Monero wallet address (see below). | | `pass` | `"x"` — the stratum password / worker name. For an open Pithead stack the default works; if the operator enabled the stack's `p2pool.stratum_password`, set this to that secret or the proxy rejects the rig. See [Pithead Integration › Stratum authentication](pithead-integration.md#stratum-authentication-optional). | -| `keepalive` | `true` | +| `rig-id` | `null` (unset) — a separate worker label for pools that support XMRig's rig identifier. It accepts the same characters as `user` and may be empty. It coexists with `user`: `user` remains the pool login and keeps its hostname default. | +| `nicehash` | `false` — enable the NiceHash Stratum protocol, where the pool assigns the nonce. Accepts only JSON `true` or `false`. | +| `algo` | `null` (automatic) — pin a RandomX algorithm. Accepted values are `rx/0`, `rx/2`, `rx/wow`, `rx/arq`, `rx/graft`, `rx/sfx`, and `rx/yada`; any other value is rejected. | +| `coin` | `null` (automatic) — let XMRig choose the algorithm for a RandomX coin by case-insensitive name or code: `monero`/`XMR`, `arqma`/`ARQ`, `graft`/`GRFT`, `wownero`/`WOW`, `zephyr`/`ZEPH`, `townforge`, or `yadacoin`/`YDA`. Unknown values and non-RandomX coins such as Sumokoin and Ravencoin are rejected. | +| `keepalive` | `true` — send Stratum keepalive requests to prevent an idle connection timing out. | | `tls` | `false` — set `true` when you connect on the pool's TLS/SSL port. | | `tls-fingerprint` | `null` (no pin) — the pool cert's SHA-256 as 64 hex chars. XMRig does no CA validation on stratum TLS, so the pin is the only server authentication; without it, TLS encrypts but doesn't authenticate. Requires `"tls": true`. See [Pithead Integration › Stratum over TLS](pithead-integration.md#stratum-over-tls-optional). | +| `sni` | `false` — send the pool hostname through TLS SNI for a name-based TLS endpoint. Accepts only JSON `true` or `false`; use it with `tls`. | | `socks5` | `null` (direct connection) — dial this pool through a SOCKS5 proxy at `host:port`, e.g. `"127.0.0.1:9050"` for a local Tor client. Same address rules as `url`. XMRig sends the pool's **hostname** to the proxy rather than resolving it first, so a v3 `.onion` stratum works with no extra setting. RigForge points the miner at a proxy; running one is yours to arrange. | -| `enabled` | `true` | +| `enabled` | `true` — allow XMRig to connect to this entry in the failover list. | | `daemon` | Omitted for Stratum; a strict boolean. Set `true` for solo mining. A `daemon+` URL implies `true`; explicit `false` with that scheme is refused. | | `daemon-poll-interval` | Omitted; XMRig defaults to 1000 milliseconds. A positive JSON integer no greater than 9007199254740991, only valid for a daemon pool. | | `daemon-job-timeout` | Omitted; XMRig defaults to 15000 milliseconds. A positive JSON integer no greater than 9007199254740991, only valid for a daemon pool. | | `daemon-zmq-port` | Omitted; optionally use the node's ZMQ notifications. An integer in 1–65535, only valid for a daemon pool. | +Absent or `null` `rig-id`, `nicehash`, `algo`, `coin`, and `sni` values are omitted from the generated config. +For `nicehash` and `sni`, an explicit `false` is preserved while `null` means unset. The algorithm and +coin allowlists are pinned to XMRig v6.26.0's `src/base/crypto/Algorithm.cpp` and +`src/base/crypto/Coin.cpp` at commit `b2ca72480c58d197e18c885d9fc1a0c8d517e60a`. + +XMRig defers to the pool when it supports algorithm negotiation, ignoring `algo` and `coin`. +Avoid `coin` with NiceHash, where the pool controls which coin is mined. + +`status` and `doctor` report the live algorithm when it is not `rx/0`. HugePages sizing and the +offline tune benchmark remain based on `rx/0`; RigForge makes no performance guarantee for another +RandomX variant. + Two common setups follow; pick the one that matches where you're mining. ### Connecting to a Pithead stack diff --git a/docs/operations.md b/docs/operations.md index 748bc13..4af25d2 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -56,7 +56,7 @@ Not every verb needs root — the design, in four lines: | `upgrade` | Redeploy this RigForge release onto the rig: regenerate the live XMRig config, reinstall the systemd units, restart so both are in effect, re-tune if periodic autotune is enabled (the fastest knobs can shift between versions), and hand the files back to you. The **compile** is the conditional part — XMRig is rebuilt when the pinned version/commit changed, the compiled-minimum marker is missing or nonzero, or the binary checksum fails. A miner you had deliberately stopped is left stopped, and the regenerated config and unit take effect at your next `start` (#413). `--check` just reports whether a newer RigForge release exists (on-demand GitHub query, always exits 0). | | `apply` | Re-read `config.json`, regenerate the live XMRig config, and restart, without recompiling. A miner that is deliberately stopped — a watchdog thermal hold, or your own `stop` — is left stopped, and picks up the new config at its next start. The fast path after editing `config.json`. On Linux it also reconciles the periodic-autotune timer with config (so changing the `autotune` target takes effect) and reports it (efficiency / performance / disabled), then asks the running miner whether a live pool connection came up — and warns when none did (a bad `pools[0].url` otherwise mines nothing while looking applied, #343). Warn only, never a failure: the pool may be legitimately down at apply time. `apply` exits non-zero when the miner restart is rejected (#630; the control path then rolls the change back instead of reporting it applied), and when the watchdog units could not be re-rendered (#395) — that one is a real failure, because the cadence the rig is running would otherwise diverge from `config.json` silently. | | `uninstall` | Remove the service and revert all system changes (fstab, limits, modules, GRUB) and the worker build/logs. Leaves `config.json`. Prompts first; add `--yes` to skip. | -| `doctor` | Read-only health check (run with `sudo` for the deepest checks). Critical findings (counted as issues): the service is active, the miner has a live pool connection (read from the miner's own local API — a running miner with no connection is not mining, #343; with the service stopped, one TCP dial of `pools[0]` stands in for a direct pool; a proxied connection cannot be verified until mining starts), HugePages are reserved, kernel lockdown isn't blocking MSR writes (read from `/sys/kernel/security/lockdown` — active lockdown means the MSR mod cannot apply, and `msr.allow_writes=on` can't override it), the `msr` module is loaded, and the MSR mod actually applied, confirmed from XMRig's log and, as root, an `rdmsr` register read-back (see [MSR mod verification](#msr-mod-verification)). Advisory findings (hints, not failures): an unreachable configured SOCKS proxy for an enabled pool (a successful TCP dial stays quiet and does not prove SOCKS/Tor health), CPU governor, 1 GB HugePages, HugePages 100%-backed (from the XMRig log), hashrate-capping hardware RigForge can't fix but you can (single-channel or slow RAM via `dmidecode`, and a power/boost-capped CPU clock), and BIOS/firmware recommendations (board/BIOS context, plus enable XMP/EXPO/DOCP or SMT when they're off; manual BIOS changes RigForge can't make from the OS). Prints an actionable hint for anything off. Also binary tamper evidence (#141): the on-disk `xmrig` is compared against the SHA-256 recorded at compile time — a deliberate rebuild refreshes the record, anything else warns and counts as an issue. When the [writable control path](#writable-control-path-opt-in) is enabled, it's also a counted issue: `doctor` checks `rigforge-control` is active and its `/status` endpoint answers (#278), staying silent when control is disabled. Exits non-zero when critical issues are found (cron-friendly, matching Pithead's `status`). | +| `doctor` | Read-only health check (run with `sudo` for the deepest checks). Critical findings (counted as issues): the service is active, the miner has a live pool connection (read from the miner's own local API — a running miner with no connection is not mining, #343; with the service stopped, one TCP dial of `pools[0]` stands in for a direct pool; a proxied connection cannot be verified until mining starts), HugePages are reserved, kernel lockdown isn't blocking MSR writes (read from `/sys/kernel/security/lockdown` — active lockdown means the MSR mod cannot apply, and `msr.allow_writes=on` can't override it), the `msr` module is loaded, and the MSR mod actually applied, confirmed from XMRig's log and, as root, an `rdmsr` register read-back (see [MSR mod verification](#msr-mod-verification)). Advisory findings (hints, not failures): the live algorithm when it differs from `rx/0`, an unreachable configured SOCKS proxy for an enabled pool (a successful TCP dial stays quiet and does not prove SOCKS/Tor health), CPU governor, 1 GB HugePages, HugePages 100%-backed (from the XMRig log), hashrate-capping hardware RigForge can't fix but you can (single-channel or slow RAM via `dmidecode`, and a power/boost-capped CPU clock), and BIOS/firmware recommendations (board/BIOS context, plus enable XMP/EXPO/DOCP or SMT when they're off; manual BIOS changes RigForge can't make from the OS). Prints an actionable hint for anything off. Also binary tamper evidence (#141): the on-disk `xmrig` is compared against the SHA-256 recorded at compile time — a deliberate rebuild refreshes the record, anything else warns and counts as an issue. When the [writable control path](#writable-control-path-opt-in) is enabled, it's also a counted issue: `doctor` checks `rigforge-control` is active and its `/status` endpoint answers (#278), staying silent when control is disabled. Exits non-zero when critical issues are found (cron-friendly, matching Pithead's `status`). | | `bench` | Run a one-off `xmrig --bench` and report the hashrate (a quick perf/health check; set `BENCH=10M` for a longer run). | | `tune` | The single command for tuning. A bare `tune` measures the fastest CPU-specific knobs (prefetch, `cpu.yield`, thread count) offline and keeps them, an optional, one-time step. Live variants: `--now` / `--short` (a quick prefetch re-tune against the running miner, the *run a live tune now* path), `--now --long` (a full live search of every knob, = `--live`), `--confirm` (A/B-check the winner live). Plus `--efficiency` / `--perf`, `--history`, `--clear`. See [Tuning](#tuning). | | `bios` | Guided, resumable walk-through of the BIOS/UEFI changes for your hardware — the settings `tune` can't reach from the OS (Secure Boot when kernel lockdown is blocking the MSR mod, memory profile XMP/EXPO/DOCP, SMT, PBO/Eco-Mode, NUMA-per-socket NPS on EPYC; `--efficiency` picks the low-power set). Detects the current firmware state via the same probes `doctor` uses, hands you a board-specific checklist one item at a time, saves the pending items, and on the next run re-verifies which changes actually took. RigForge never writes BIOS itself; plan for console access (keyboard/KVM) for the reboot-into-BIOS step. Linux-only. See [Guided BIOS tuning](#guided-bios-tuning). | @@ -64,7 +64,7 @@ Not every verb needs root — the design, in four lines: | `watchdog` | One health check per run — the scheduled recovery verb, like `autotune`. You normally don't type it; set `"watchdog": "enabled"` in `config.json` and setup installs a timer that runs it every `watchdog_interval_min` minutes: two consecutive checks seeing 0 H/s or a dead API restart the wedged miner, and with `max_temp_c` set it stops the miner above that temperature (starting it again 5 °C below). Linux-only. See [Watchdog](#watchdog-opt-in). | | `backup` | Snapshot `config.json` + the tuning files into a timestamped `tar.gz` under `./backups`. See [Backup & restore](#backup--restore). | | `restore` | Restore `config.json` + tuning from a backup archive: `restore [-y] `. Prompts before overwriting. | -| `status` | One-glance live summary from the worker API (hashrate, pool, uptime, accepted/rejected shares — one fetch, no sudo), then the systemd service status. Miner stopped or API off: a single "worker API not reachable" line and the platform block as before. | +| `status` | One-glance live summary from the worker API (hashrate, pool, uptime, accepted/rejected shares, and the live algorithm when it differs from `rx/0` — one fetch, no sudo), then the systemd service status. Miner stopped or API off: a single "worker API not reachable" line and the platform block as before. | | `logs` | Follow the live service logs (`journalctl -f`). | | `start` / `stop` / `restart` | Start, stop, or restart the miner service. (`up` / `down` are aliases for `start` / `stop`.) | | `enable` / `disable` | Start the service on boot, or not. | diff --git a/docs/pithead-integration.md b/docs/pithead-integration.md index 81cdd4b..3aa5ae3 100644 --- a/docs/pithead-integration.md +++ b/docs/pithead-integration.md @@ -113,6 +113,9 @@ pithead#235): Coordinators can use `rigforge.appliance` to hide git-based upgrade controls on appliance rigs and show `rigforge.stamp` as the baked release provenance instead. +Coordinators can use `rigforge.appliance` to hide git-based upgrade controls on appliance rigs and +show `rigforge.stamp` as the baked release provenance instead. + When a token is set, `:8081` accepts either the exact bearer for existing clients or the lowercase hex HMAC-SHA256 derived with that token as the key and `rigforge:api-read:v1` as the message. The derived bearer grants reads only: `:8082` rejects it and continues to require an exact diff --git a/rigforge.sh b/rigforge.sh index 1c0a281..6063978 100755 --- a/rigforge.sh +++ b/rigforge.sh @@ -447,19 +447,10 @@ parse_config() { }) ' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON." - # tls-fingerprint (#115): re-attach the pin from the raw config, emitted ONLY when set — adding - # it unconditionally (null) would change the generated config shape for every existing rig on - # its next apply. A second single-line pass rather than lines inside the map above: kcov can't - # attribute in-string program lines, and the patch-coverage gate needs every new line hittable. POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | ."tls-fingerprint"]] | transpose | map(.[0] + (if (.[1] // null) != null then {"tls-fingerprint": .[1]} else {} end))' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON." - - # Re-attach socks5 only when set, using the same shape/coverage rule as tls-fingerprint. POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | .socks5]] | transpose | map(.[0] + (if (.[1] // null) != null then {"socks5": .[1]} else {} end))' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON." + POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | {"rig-id": .["rig-id"], nicehash, algo, coin, sni} | with_entries(select(.value != null))]] | transpose | map(.[0] + .[1])' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON." - # #265: jq's `//` treats an explicit false like null/missing, so the map above rewrites an - # operator's "keepalive": false / "enabled": false to the true default. Restore explicit falses - # from the raw config in another single-line pass (same kcov rationale as #115 above). Only a - # boolean false matches `== false`; non-boolean junk flows through `//` untouched to validation. POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | [(.keepalive == false), (.enabled == false)]]] | transpose | map(.[0] + (if .[1][0] then {"keepalive": false} else {} end) + (if .[1][1] then {"enabled": false} else {} end))' "$CONFIG_JSON") || error "Could not parse 'pools' in $CONFIG_JSON." POOLS_JSON=$(jq -c --argjson base "$POOLS_JSON" '[$base, [.pools[] | with_entries(select(.key == "daemon" or .key == "daemon-poll-interval" or .key == "daemon-job-timeout" or .key == "daemon-zmq-port") | if (.key != "daemon") and (.value | type == "number") and (.value == (.value | floor)) then .value |= floor else . end)]] | transpose | map((.[0] + .[1]) | if (.url | type == "string" and (startswith("daemon+http://") or startswith("daemon+https://"))) and (has("daemon") | not) then .daemon = true else . end)' "$CONFIG_JSON") || error "Could not parse daemon pool fields." @@ -490,14 +481,20 @@ parse_config() { if [ -n "$_user" ] && ! [[ "$_user" =~ ^[A-Za-z0-9._:@+-]+$ ]]; then error "Pool user '$_user' has invalid characters (allowed: letters, digits, . _ - : @ +)." fi - # #415: `pass` must be a STRING, checked before the charset rule below can be fooled. The - # feed masks a set password to the {"__secret__": true} sentinel and _control_commit - # resolves it back, but nothing stops a marker — or any other JSON object — reaching - # config.json by a hand edit or by a future writer that forgets to resolve one. `jq -r` - # renders an object as its JSON text, and the empty object `{}` renders as two graph - # characters on one line, so it passes the check below and becomes the rig's literal - # password. Type-check here so the guarantee does not depend on every writer remembering: - # the same posture as the non-string tls-fingerprint rejection below. + if ! jq -e 'has("rig-id") == false or (."rig-id" | type == "string" and (test("[^A-Za-z0-9._:@+-]") | not))' <<<"$_pool" >/dev/null 2>&1; then + error "Pool rig-id must be a string using only these characters: letters, digits, . _ - : @ + (or empty)." + fi + for _f in nicehash sni; do + if ! jq -e --arg f "$_f" 'has($f) == false or (.[$f] | type == "boolean")' <<<"$_pool" >/dev/null 2>&1; then + error "Pool $_f must be true or false." + fi + done + if ! jq -e '.algo == null or (.algo | IN("rx/0", "rx/2", "rx/wow", "rx/arq", "rx/graft", "rx/sfx", "rx/yada"))' <<<"$_pool" >/dev/null 2>&1; then + error "Pool algo must be a supported RandomX algorithm (rx/0, rx/2, rx/wow, rx/arq, rx/graft, rx/sfx, rx/yada): HugePages reservation and tune --bench assume a RandomX scratchpad." + fi + if ! jq -e '.coin == null or (.coin | ascii_downcase | IN("monero", "xmr", "arqma", "arq", "graft", "grft", "wownero", "wow", "zephyr", "zeph", "townforge", "yadacoin", "yda"))' <<<"$_pool" >/dev/null 2>&1; then + error "Pool coin must be a supported RandomX coin (monero/XMR, arqma/ARQ, graft/GRFT, wownero/WOW, zephyr/ZEPH, townforge, yadacoin/YDA): HugePages reservation and tune --bench assume a RandomX scratchpad." + fi if [ "$(jq -r '.pass | type' <<<"$_pool")" != "string" ]; then error "Pool pass must be a string (got a $(jq -r '.pass | type' <<<"$_pool")). A masked-secret marker from the API feed is not a password — send the real value, or omit 'pass' entirely." fi @@ -736,7 +733,7 @@ parse_config() { # #1 image seed. Warn NAMES only, never values — a fat-fingered token must not land in a log. _warn_unknown_config_keys() { local known="pools ACCESS_TOKEN ACCESS_TOKENS DONATION autotune add_to_path HOME_DIR api api_port api_bind api_allow_from miner_user RIG_NAME watchdog watchdog_interval_min max_temp_c control control_port control_bind control_upgrade hugepages_reserve_extra_mb hugepages_pool_ceiling_mb threads shared_host" - local known_pool="url user pass keepalive tls enabled tls-fingerprint socks5 daemon daemon-poll-interval daemon-job-timeout daemon-zmq-port" + local known_pool="url user pass keepalive tls enabled tls-fingerprint socks5 rig-id nicehash algo coin sni daemon daemon-poll-interval daemon-job-timeout daemon-zmq-port" local k lk m lm hit hint unknown_seen=0 while IFS= read -r k; do case "$k" in _*) continue ;; esac @@ -3965,8 +3962,9 @@ mac_disable() { fi } -# One-glance live summary from a single /2/summary fetch (#143). Facts only, no ✓/! judgment -# markers (that's doctor's job); plain aligned lines stay grep-friendly. Never sudo, never prompts. +_print_pool_algorithm() { + jq -r '.algo | select(type == "string" and . != "" and . != "rx/0") | " Algorithm: \(.)"' 2>/dev/null || true +} _status_api_summary() { local body hs pool up acc rej hp body=$(_read_api_summary) @@ -3982,6 +3980,7 @@ _status_api_summary() { [ -n "${hs:-}" ] || return 0 # half-up API / unparseable body: stay quiet, platform block follows printf ' %-10s %s H/s\n' "Hashrate:" "$hs" printf ' %-10s %s\n' "Pool:" "$pool" + printf '%s' "$body" | _print_pool_algorithm printf ' %-10s %s\n' "Uptime:" "$(_render_duration "$up")" printf ' %-10s %s accepted / %s rejected\n' "Shares:" "$acc" "$rej" [ -n "$hp" ] && printf ' %-10s %s\n' "HugePages:" "$hp" @@ -5418,6 +5417,7 @@ doctor() { fi log "Checking the worker (read-only)..." _config_apply_check + case "$(jq -r 'if (.control | tostring) | test("^(enabled|true|on)$") then "enabled" else (.api // "disabled") end' "$CONFIG_JSON" 2>/dev/null || true)" in # #507: doctor has no parse_config, so it re-derives control's api implication here enabled | true | on) local refresh_state @@ -5454,6 +5454,7 @@ doctor() { _tcp_probe "$ph" "$pp" || _ck_warn "configured SOCKS proxy is unreachable (TCP connect failed) — check pools[].socks5 and install/start Tor if needed" socks-proxy done < <(jq -r '[.pools[]? | select(.enabled != false) | .socks5 | select(. != null and . != "")] | unique[] | if type == "string" and (test("[[:cntrl:]]") | not) then . else "invalid proxy" end' "$CONFIG_JSON" 2>/dev/null) if [ "$svc_up" = y ]; then + ACCESS_TOKEN="$(jq -r '.ACCESS_TOKEN // empty' "$CONFIG_JSON" 2>/dev/null || true)" _read_api_summary | _print_pool_algorithm if ! ACCESS_TOKEN="$(jq -r '.ACCESS_TOKEN // empty' "$CONFIG_JSON" 2>/dev/null || true)" _check_donation; then issues=$((issues + 1)) fi diff --git a/tests/README.md b/tests/README.md index 59d3882..5b3bd27 100644 --- a/tests/README.md +++ b/tests/README.md @@ -57,6 +57,9 @@ CI median/p90, failures and occupied hours; missing evidence leaves rollout acce The opt-in `shared-host` phase of `e2e-pithead.sh` is not part of `all` and runs as a `rigforge`/`tier4-e2e` bench-ci job; [docs/shared-host.md](../docs/shared-host.md) describes what it checks and restores. +The opt-in `shared-host` phase of `e2e-pithead.sh` is not part of `all` and runs as a `rigforge`/`tier4-e2e` +bench-ci job; [docs/shared-host.md](../docs/shared-host.md) describes what it checks and restores. + `tests/test-sister-reconcile.sh`, registered in `tests/run.sh`, checks startup failures, bounded listener probes (including IPv6 and failed transports), and apply's failure result while retaining later reconciles. The hosted Ubuntu 24.04 `Stock home setup` jobs run historical and current revisions on separate diff --git a/tests/run.sh b/tests/run.sh index e091cf9..a9403b0 100644 --- a/tests/run.sh +++ b/tests/run.sh @@ -379,6 +379,8 @@ assert_eq "second-pool socks5 stays on the second pool (#400)" "$(PJ "$c" | jq - # A pool may carry BOTH a pin and a proxy — the two re-attach passes must not clobber each other. c="$(mkconf p_s5fp "{ \"pools\": [{\"url\":\"h:443\",\"tls\":true,\"tls-fingerprint\":\"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"socks5\":\"127.0.0.1:9050\"}] }")" assert_eq "socks5 and tls-fingerprint coexist (#400)" "$(PJ "$c" | jq -c '[(.[0]."tls-fingerprint" != null), (.[0].socks5 == "127.0.0.1:9050")]')" "[true,true]" +bash "$ROOT/tests/test-pool-fields.sh" "$SCRIPT" "$ROOT/tests/contract/v1/feed.json" +assert_rc "XMRig optional pool fields round-trip and validate (#527)" "$?" 0 # #400 validation: the socks5 address gets the SAME host:port rules as the pool url. Each of these # is rejected for the pool url already; assert the proxy is judged by the same standard. c="$(mkconf p_s5noport "{ \"pools\": [{\"url\":\"h:3333\",\"socks5\":\"127.0.0.1\"}] }")" @@ -497,7 +499,6 @@ assert_rc "no pools rejected" "$?" "1" c="$(mkconf p_emptypools "{ \"pools\": [] }")" parse_rc "$c" "$ROOT" assert_rc "empty pools array rejected" "$?" "1" - # Every config field is validated — bad input fails fast with a clear message rather than producing a # config XMRig would choke on. echo "== unit: config field sanitization ==" diff --git a/tests/test-pool-fields.sh b/tests/test-pool-fields.sh new file mode 100644 index 0000000..60ec69a --- /dev/null +++ b/tests/test-pool-fields.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1090,SC2034,SC2329 +set -uo pipefail +SCRIPT="$1" +FEED="$2" +D=$(mktemp -d) +trap 'rm -rf "$D"' EXIT +FAIL=0 +pass() { printf ' ok: %s\n' "$1"; } +fail() { + printf ' FAIL: %s%s\n' "$1" "${2:+ — $2}" >&2 + FAIL=$((FAIL + 1)) +} +eq() { + if [ "$2" = "$3" ]; then pass "$1"; else fail "$1" "expected [$3], got [$2]"; fi +} +BASE_POOL=$(jq -c '.rigforge.config.pools[0] | del(.pass)' "$FEED") +FIXTURE_ID=$(jq -r '.rigforge.control.change_id' "$FEED") +write_config() { jq -n --argjson pools "$2" '{pools: $pools}' >"$1"; } +parse() { + write_config "$D/input.json" "$1" + ( + source "$SCRIPT" + CONFIG_JSON="$D/input.json" SCRIPT_DIR=$(dirname "$SCRIPT") + parse_config + ) 2>&1 +} +has() { case "$2" in *"$3"*) pass "$1" ;; *) fail "$1" "missing [$3]" ;; esac } +absent() { case "$2" in *"$3"*) fail "$1" "unexpected [$3]" ;; *) pass "$1" ;; esac } +rejects() { + local label="$1" key="$2" value="$3" fragment="$4" pools out rc + pools=$(jq -cn --argjson base "$BASE_POOL" --argjson value "$value" --arg key "$key" '[($base + {($key): $value})]') + out=$(parse "$pools") + rc=$? + if [ "$rc" -eq 0 ]; then + fail "$label" "accepted invalid value" + return + fi + has "$label names the reason" "$(printf '%s' "$out" | tr '[:upper:]' '[:lower:]')" "$(printf '%s' "$fragment" | tr '[:upper:]' '[:lower:]')" +} +render() { + local rc + write_config "$D/input.json" "$1" + rm -rf "$D/worker" + mkdir -p "$D/logrotate" + ( + RIGFORGE_HOME=$(dirname "$SCRIPT") LOGROTATE_DIR="$D/logrotate" source "$SCRIPT" + CONFIG_JSON="$D/input.json" SCRIPT_DIR=$(dirname "$SCRIPT") OS_TYPE=Linux LOGROTATE_DIR="$D/logrotate" + parse_config >/dev/null + WORKER_ROOT="$D/worker" CPUINFO="$D/no-cpuinfo" + mkdir -p "$WORKER_ROOT/xmrig/build" + lscpu() { printf 'Model name: fixture cpu\n'; } + hostname() { printf '%s\n' "$FIXTURE_ID"; } + sudo() { "$@"; } + cd "$WORKER_ROOT/xmrig/build" || exit + generate_xmrig_config >/dev/null + ) 2>/dev/null + rc=$? + if [ "$rc" -ne 0 ]; then + fail "generated config render" "exited $rc" + return "$rc" + fi + cp "$D/worker/xmrig/build/config.json" "$2" +} +echo "== pool schema fields (#527) ==" +FP=$(printf '%064d' 0) +POOLS=$(jq -cn --argjson base "$BASE_POOL" --arg fp "$FP" '[ + ($base + {"rig-id":"Rig_One.:@+-","nicehash":false,"sni":true,"algo":"rx/wow","coin":"WOW","tls":true,"tls-fingerprint":$fp}), + ($base + {"rig-id":"","nicehash":true,"sni":false,"algo":"rx/arq","coin":"arqma","keepalive":false,"enabled":false}) +]') +render "$POOLS" "$D/all.json" +eq "new pool fields stay on their source index" "$(jq -c '[.pools[] | [."rig-id",.nicehash,.sni,.algo,.coin]]' "$D/all.json")" '[["Rig_One.:@+-",false,true,"rx/wow","WOW"],["",true,false,"rx/arq","arqma"]]' +eq "new fields coexist with older optional fields" "$(jq -r '.pools[0]."tls-fingerprint"' "$D/all.json")" "$FP" +eq "explicit false values survive generation" "$(jq -c '[.pools[0].nicehash,.pools[1].sni,.pools[1].keepalive,.pools[1].enabled]' "$D/all.json")" '[false,false,false,false]' +eq "rig-id coexists with the unchanged default user" "$(jq -r '.pools[0].user' "$D/all.json")" "$FIXTURE_ID" +out=$(parse "$POOLS") +absent "new fields do not trigger unknown-field warnings" "$out" "unknown pool field" +LEGACY=$(jq -cn --argjson base "$BASE_POOL" '[$base]') +NULLS=$(jq -cn --argjson base "$BASE_POOL" '[($base + {"rig-id":null,nicehash:null,sni:null,algo:null,coin:null})]') +render "$LEGACY" "$D/legacy.json" +render "$NULLS" "$D/nulls.json" +if cmp -s "$D/legacy.json" "$D/nulls.json"; then pass "null fields leave generated config byte-identical"; else fail "null fields leave generated config byte-identical"; fi +eq "absent fields remain absent" "$(jq -c '[.pools[0] | has("rig-id"),has("nicehash"),has("sni"),has("algo"),has("coin")]' "$D/legacy.json")" '[false,false,false,false,false]' + +for value in true 7 '{}' '[]'; do rejects "rig-id rejects non-string $value" rig-id "$value" string; done +rejects "rig-id rejects whitespace" rig-id '"bad value"' characters +rejects "rig-id rejects a slash" rig-id '"bad/value"' characters +for key in nicehash sni; do + rejects "$key rejects string false" "$key" '"false"' "$key" + rejects "$key rejects string true" "$key" '"true"' "$key" + rejects "$key rejects a number" "$key" 1 "$key" +done + +for algo in rx/0 rx/2 rx/wow rx/arq rx/graft rx/sfx rx/yada; do + pools=$(jq -cn --argjson base "$BASE_POOL" --arg algo "$algo" '[($base + {algo:$algo})]') + if parse "$pools" >/dev/null; then pass "algo $algo accepted"; else fail "algo $algo accepted"; fi +done +rejects "non-RandomX algo rejected" algo '"kawpow"' RandomX +pools=$(jq -cn --argjson base "$BASE_POOL" '[($base + {algo:"kawpow"})]') +out=$(parse "$pools") +has "algo diagnostic explains the HugePages constraint" "$out" HugePages +has "algo diagnostic explains the tune constraint" "$out" tune +has "algo diagnostic explains the scratchpad constraint" "$out" scratchpad +rejects "unknown RandomX algo rejected" algo '"rx/future"' supported + +for coin in monero XMR arqma ARQ graft GRFT wownero WOW zephyr ZEPH townforge yadacoin YDA; do + pools=$(jq -cn --argjson base "$BASE_POOL" --arg coin "$coin" '[($base + {coin:$coin})]') + if parse "$pools" >/dev/null; then pass "coin $coin accepted"; else fail "coin $coin accepted"; fi +done +for coin in ravencoin RVN sumokoin SUMO unknown; do rejects "coin $coin rejected" coin "\"$coin\"" coin; done + +summary_output() { + local algo="$1" entrypoint="$2" body="$D/summary.json" + jq --arg algo "$algo" '.algo = $algo' "$FEED" >"$body" + ( + source "$SCRIPT" + API_CMD="cat '$body'" ACCESS_TOKEN="" + if [ "$entrypoint" = status ]; then + _status_api_summary + else + OS_TYPE=Linux CONFIG_JSON="$D/doctor.json" SERVICE_NAME=fixture-service + MEMINFO="$D/none/meminfo" MSR_MODULE_DIR="$D/none/msr" GOVERNOR_FILE="$D/none/governor" + HUGEPAGES_1G_NR="$D/none/huge1g" CPUINFO="$D/none/cpuinfo" CPUFREQ_MAX="$D/none/freq" + LOCKDOWN_FILE="$D/none/lockdown" DMI_DIR="$D/none/dmi" NODE_SYSFS="$D/none/node" + THERMAL_ZONE="$D/none/thermal" HWMON_DIR="$D/none/hwmon" RDMSR_BIN="$D/none/rdmsr" + systemctl() { [ "${1:-}" = is-active ]; } + lscpu() { printf 'Model name: fixture cpu\n'; } + _check_donation() { return 0; } + doctor || true + fi + ) 2>&1 +} + +write_config "$D/doctor.json" "$LEGACY" +for entrypoint in status doctor; do + out=$(summary_output rx/wow "$entrypoint") + has "$entrypoint shows a live non-default algorithm" "$out" "Algorithm:" + has "$entrypoint names the live non-default algorithm" "$out" "rx/wow" + out=$(summary_output rx/0 "$entrypoint") + absent "$entrypoint omits the default algorithm" "$out" "Algorithm:" +done +jq 'del(.algo)' "$FEED" >"$D/summary.json" +out=$( ( + source "$SCRIPT" + API_CMD="cat '$D/summary.json'" ACCESS_TOKEN="" + _status_api_summary +) 2>&1) +absent "status is silent when the live algorithm is missing" "$out" "Algorithm:" +jq '.algo = 7' "$FEED" >"$D/summary.json" +out=$( ( + source "$SCRIPT" + API_CMD="cat '$D/summary.json'" ACCESS_TOKEN="" + _status_api_summary +) 2>&1) +absent "status is silent when the live algorithm is invalid" "$out" "Algorithm:" + +CHANGE=$(jq -cn --argjson pools "$POOLS" '{pools:$pools}') +mkdir -p "$D/state/spool" "$D/backups" +printf '%s' "$CHANGE" >"$D/change.json" +STAGED="$D/change.json" +if command -v python3 >/dev/null 2>&1; then + PYTHONDONTWRITEBYTECODE=1 python3 - "$SCRIPT" "$D/state/spool" "$D/change.json" <<'PY' +import importlib.util, json, pathlib, sys +path = pathlib.Path(sys.argv[1]).parent / "util/control-server.py" +spec = importlib.util.spec_from_file_location("control_server", path) +module = importlib.util.module_from_spec(spec) +spec.loader.exec_module(module) +payload = pathlib.Path(sys.argv[3]).read_bytes() +cid = module.stage_change(sys.argv[2], payload) +staged = pathlib.Path(sys.argv[2], "pending-" + cid + ".json").read_bytes() +assert json.loads(staged) == json.loads(payload) +PY + eq "control receiver stages every pool field" "$?" 0 + STAGED=$(find "$D/state/spool" -name 'pending-*.json' -print -quit) +else + printf ' SKIP: python3 unavailable; control receiver staging not run\n' +fi +write_config "$D/control.json" "$LEGACY" +( + source "$SCRIPT" + CONFIG_JSON="$D/control.json" SCRIPT_DIR=$(dirname "$SCRIPT") RIGFORGE_APPLIANCE=0 + _fsync_paths() { :; } + _control_commit "$STAGED" "$D/backups" >/dev/null +) +eq "control apply accepts the same valid pool fields" "$?" 0 +eq "control apply preserves all staged fields" "$(jq -c '.pools | map({"rig-id",nicehash,sni,algo,coin})' "$D/control.json")" '[{"rig-id":"Rig_One.:@+-","nicehash":false,"sni":true,"algo":"rx/wow","coin":"WOW"},{"rig-id":"","nicehash":true,"sni":false,"algo":"rx/arq","coin":"arqma"}]' + +BAD_CHANGE=$(jq -cn --argjson base "$BASE_POOL" '{pools:[($base + {algo:"kawpow"})]}') +printf '%s' "$BAD_CHANGE" >"$D/bad-change.json" +before=$(jq -cS . "$D/control.json") +out=$( ( + source "$SCRIPT" + CONFIG_JSON="$D/control.json" SCRIPT_DIR=$(dirname "$SCRIPT") RIGFORGE_APPLIANCE=0 + _fsync_paths() { :; } + _control_commit "$D/bad-change.json" "$D/backups" +) 2>&1) +rc=$? +if [ "$rc" -ne 0 ]; then pass "control apply uses shared pool validation"; else fail "control apply uses shared pool validation"; fi +has "control rejection reports invalid config" "$out" "rejected invalid-config" +eq "rejected control apply leaves config unchanged" "$(jq -cS . "$D/control.json")" "$before" +[ "$FAIL" -eq 0 ] || exit 1 +printf 'pool schema fields: PASS\n'