Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,33 @@ All notable changes to RigForge are documented here. The format is based on

## [Unreleased]

## [1.16.0] - 2026-08-23

### Added

- **Control API: a restart-free fast path for `watchdog_interval_min` and `max_temp_c` (#381, from
#344 item 1).** `control-apply` used to re-run the entire `apply` pipeline for every accepted
change — regenerate XMRig's config, re-render its unit, restart the service, then poll for a live
pool connection — even for a change that never touches XMRig at all. A change whose keys are
*only* `watchdog_interval_min` and/or `max_temp_c` now reconciles just the watchdog timer and
leaves XMRig running, closing the ~62s gap the original walkthrough measured for a single-key
change. The allowlist is closed and checked as a subset match (a key not on it, including any
future addition to the control-writable set, still takes the full path), and a fast-path failure
falls back to the same full-pipeline rollback a failed restart already uses.

### Fixed

- **A new `hugepages_pool_ceiling_mb` bounds the grow-only HugePages write to a declared ceiling
(#398).** `hugepages_reserve_extra_mb` adds declared co-resident headroom into the computed
requirement; the grow-only write's availability check gives no credit back for pages a *different*
consumer (a co-hosted Pithead stack) already holds from that same headroom, so the two combine
into a double count no value of `hugepages_reserve_extra_mb` can correct — traced to an ~12 GiB
pool request on an 8 GiB box (pithead#1103). `hugepages_pool_ceiling_mb` (default `0`, no
ceiling) caps the write itself instead: when declared, `vm.nr_hugepages` is never grown past the
ceiling regardless of how the requirement/availability arithmetic comes out — an odd declared MB
value floors to the 2MB page below rather than rounding up past it. Inert unless a caller sets
it — every existing config computes exactly as before.

## [1.15.2] - 2026-08-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.15.2
1.16.0
3 changes: 2 additions & 1 deletion config.reference.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"_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. 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). '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; '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",
"_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. 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). '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); '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",

"pools": [
{ "url": "<YOUR_POOL_HOST>:3333", "user": "", "pass": "x", "keepalive": true, "tls": false, "tls-fingerprint": null, "enabled": true }
Expand All @@ -26,6 +26,7 @@
"add_to_path": false,

"hugepages_reserve_extra_mb": 0,
"hugepages_pool_ceiling_mb": 0,
"threads": null,

"HOME_DIR": "DYNAMIC_HOME"
Expand Down
6 changes: 6 additions & 0 deletions docs/adr/0001-writable-worker-config-control-path.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ D7 stamps `source: "control"` on a control apply. #254 extends the provenance to

The accepted→poll contract (D2) has a race: a concurrent change between a caller's `POST` and its poll makes the no-arg `GET /status` report the *newer* change, so the caller can't confirm its own. #255 adds `GET /status?change_id=<16hex>` returning that change's recorded outcome (or `404`) — the applier already writes each outcome, so it additionally indexes them under `changes/<change_id>.json` (last ~20; the id is server-generated 16-hex and re-validated before it becomes a path component). The no-arg form stays most-recent for compatibility; auth is unchanged. Chosen over a `/changes` ring-buffer endpoint (issue Option B) as the smaller, direct change.

### D12. A restart-free fast path for a closed subset of the allowlist (#381, from #344 item 1)

D6's `apply` re-run is correct as a baseline but expensive: a live walkthrough measured a single `watchdog_interval_min` change taking ~62s round-trip through `POST /apply`, because the applier re-runs the *entire* `apply` pipeline — regenerate XMRig's config, re-render its unit, restart the service, then poll for a live pool connection — for every change, even one that never touches XMRig at all.

The #344 discussion set one constraint before this could land: the fast path must not fork a second apply implementation that can drift from the real one. Decision: a closed allowlist (`watchdog_interval_min`, `max_temp_c`), checked as a **subset** match — every changed key must be on it, never a "not otherwise restart-requiring" complement — so a future addition to the D3 allowlist that nobody has re-proven restart-free here takes the full path by construction. Both current members are proven restart-free from the applier's own code, not asserted: `watchdog_interval_min` bakes into *only* `rigforge-watchdog.timer`'s cadence, and `max_temp_c` is never rendered into a unit at all — the watchdog verb re-reads `config.json` on every scheduled run. Neither reaches XMRig's generated config or its unit template. The fast path reuses `install_watchdog` — the same call the full `apply` pipeline already makes on every run — rather than re-implementing unit rendering, so the two paths cannot drift on what "restart-free" renders; it still stamps `config_meta` provenance (D10) the same way `apply` does, so a consumer cannot tell which path served a change from the feed alone. Success is a *run-state* comparison, not a bare is-active snapshot: the applier records whether the miner service was active *before* the fast apply and again *after*, and only a transition from active to inactive counts as failure. A rig can be legitimately stopped when a restart-free change lands — a watchdog thermal hold (D8/#257), or an operator's manual stop — and since the fast path never touches the XMRig unit or service, a stopped rig staying stopped (or even coming back on its own) is not this change's doing and must not be read as a fast-path failure; the new value still takes effect on the watchdog's next scheduled tick. An adversarial review of the first version of this decision found it gated on is-active alone, which would have discarded the operator's change and force-restarted a rig that was deliberately offline — exactly the thermal-hold-plus-`max_temp_c`-edit case this fast path exists for. A failure (active before, inactive after) falls through to the *same* full-pipeline rollback D6 already defines, never a bespoke recovery path. `pools`, `DONATION`, `autotune`, and the `watchdog` enable/disable flag stay on the full path: the first two are XMRig's own served config, and the latter two touch `install_*` behaviour this issue did not audit for restart-freedom.

## Alternatives considered

- **Write verbs on the sister API.** Rejected: violates the read-only invariant, and the `DynamicUser` read process cannot persist or apply.
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ not read by `parse_config` today, so it isn't in the table below.)
| `api` | `"disabled"` | `"enabled"` serves the sister API: a second **read-only** port with XMRig's `/1/summary`+`/2/summary` passed through verbatim plus a namespaced `rigforge` object (tune state, RAPL watts, firmware/health probes, pinned versions), and `/health` + `/tune` endpoints. One tiny persistent stdlib server; a systemd timer refreshes its data every 15 s, so requests never touch the miner (see [operations › sister API](operations.md)). Gated by the same `ACCESS_TOKEN`; Linux-only. |
| `api_port` | `8081` | Sister API port (8080 is rejected — that's XMRig's own API). |
| `api_bind` | `"0.0.0.0"` | Sister API listen address. |
| `control` | `"disabled"` | `"enabled"` serves the **writable** control path (#236): a *separate* authenticated port that lets a Pithead stack apply config changes through RigForge, so `config.json` stays the source of truth (the producer for pithead Worker Inspect). **Fail-closed:** enabling it requires *both* `ACCESS_TOKEN` and `api_allow_from` — a writable API with no token or no pinned source is refused with a hard error. Only `pools`, `DONATION`, `autotune`, `watchdog`(+`watchdog_interval_min`), and `max_temp_c` are writable through it; anything else is rejected. The **remote** path additionally 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` on the box (#257). Each change is validated, the old config is snapshotted to `config-backups/` first, and a change that doesn't come back live is rolled back. The receiver holds no privilege and stages off the request path, so writes never touch mining. Linux-only. See [Operations › Control path](operations.md#writable-control-path-opt-in). |
| `control` | `"disabled"` | `"enabled"` serves the **writable** control path (#236): a *separate* authenticated port that lets a Pithead stack apply config changes through RigForge, so `config.json` stays the source of truth (the producer for pithead Worker Inspect). **Fail-closed:** enabling it requires *both* `ACCESS_TOKEN` and `api_allow_from` — a writable API with no token or no pinned source is refused with a hard error. Only `pools`, `DONATION`, `autotune`, `watchdog`(+`watchdog_interval_min`), and `max_temp_c` are writable through it; anything else is rejected. A change touching only `watchdog_interval_min` and/or `max_temp_c` applies without restarting XMRig (#381) — every other key restarts it. The **remote** path additionally 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` on the box (#257). Each change is validated, the old config is snapshotted to `config-backups/` first, and a change that doesn't come back live is rolled back. The receiver holds no privilege and stages off the request path, so writes never touch mining. Linux-only. See [Operations › Control path](operations.md#writable-control-path-opt-in). |
| `control_port` | `8082` | Control path port (rejects 8080 and the `api_port`). |
| `control_bind` | `"0.0.0.0"` | Control path listen address. Pair with `api_allow_from` (required) to pin who may write. |
| `miner_user` | `""` *(root)* | Run the miner as this dedicated non-root system user (created at setup, nologin). RigForge applies the CPU's MSR preset root-side before start; on families without a known preset the ~10-15% MSR boost is skipped — which is why this ships opt-in. Lowering privilege changes nothing else: HugePages come from the boot reservation, tune/doctor/apply all keep working. |
Expand Down
Loading