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
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,28 @@ All notable changes to RigForge are documented here. The format is based on

## [Unreleased]

## [1.14.0] - 2026-08-02

### Added

- **Missing AES-NI / AVX2 is surfaced instead of mining silently slow (#338).** RandomX without
AES-NI falls back to XMRig's soft-AES path, roughly 4x slower, and nothing anywhere said why — the
last undelivered acceptance criterion from #1. `setup`/`apply` now warn at configure time when the
CPU flags lack `aes` (and, advisory, `avx2` — that one only slows dataset init), and `doctor`
counts a missing AES-NI as an issue. Judged only when an x86-style `flags` line exists in
`/proc/cpuinfo`; macOS, ARM and stubbed sandboxes read as unknown, and unknown never manufactures
an issue (the #333 lockdown stance). Never aborts: a knowingly-old rig is a valid choice.

### Fixed

- **Debian: dependency install no longer fails on `linux-tools-common` (#327).** The apt dependency
list named `linux-tools-common` unconditionally, but that package is Ubuntu-only — Debian ships
cpupower as `linux-cpupower` — and apt's install transaction is all-or-nothing, so the one unknown
name failed the ENTIRE install: git, build-essential and cmake never arrived either. `setup` now
probes with `apt-cache show` (the same guard the kernel-versioned `linux-tools-$(uname -r)`
package already had) and adds whichever name the distro actually ships; when neither exists it
warns and moves on — cpupower is a tuning aid, never worth losing the compiler toolchain over.

## [1.13.1] - 2026-08-01

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.13.1
1.14.0
56 changes: 55 additions & 1 deletion rigforge.sh
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ BIN_DIR="${BIN_DIR:-/usr/local/bin}"

# Read-only system paths the `doctor` health check inspects (overridable for tests).
MEMINFO="${MEMINFO:-/proc/meminfo}"
# CPU flags source for the ISA preflight (#338). Same override name util/proposed-grub.sh already
# uses for its pdpe1gb probe — one knob, and the test harness already isolates it from the host.
CPUINFO="${CPUINFO:-/proc/cpuinfo}"
MSR_MODULE_DIR="${MSR_MODULE_DIR:-/sys/module/msr}"
GOVERNOR_FILE="${GOVERNOR_FILE:-/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor}"
HUGEPAGES_1G_NR="${HUGEPAGES_1G_NR:-/sys/kernel/mm/hugepages/hugepages-1048576kB/nr_hugepages}"
Expand Down Expand Up @@ -738,7 +741,20 @@ _detect_pkg_manager() {
DEP_LIST="git build-essential cmake libuv1-dev libssl-dev libhwloc-dev gettext-base python3"
if [ "$OS_TYPE" == "Linux" ]; then
# msr-tools (rdmsr): lets `doctor` verify the prefetcher MSR mod actually applied (#66).
DEP_LIST="$DEP_LIST linux-tools-common msr-tools"
DEP_LIST="$DEP_LIST msr-tools"
# cpupower's package name differs by distro: linux-tools-common on Ubuntu,
# linux-cpupower on Debian. apt's install transaction is all-or-nothing, so one unknown
# name would fail the ENTIRE dependency install — gcc/cmake included (#327). Probe with
# apt-cache show (same guard as the kernel-versioned package below) and add only the
# name this distro actually ships; when neither exists, warn and carry on — cpupower is
# a tuning aid, never worth losing the compiler toolchain over.
if apt-cache show linux-tools-common &>/dev/null; then
DEP_LIST="$DEP_LIST linux-tools-common"
elif apt-cache show linux-cpupower &>/dev/null; then
DEP_LIST="$DEP_LIST linux-cpupower"
else
warn "No cpupower package found (tried linux-tools-common, linux-cpupower) — skipping it. Frequency tuning via cpupower may be unavailable."
fi
if apt-cache show "linux-tools-$(uname -r)" &>/dev/null; then
DEP_LIST="$DEP_LIST linux-tools-$(uname -r)"
fi
Expand Down Expand Up @@ -988,6 +1004,13 @@ generate_xmrig_config() {
# where only one CCD has the V-cache). See issue #44.
if [ "$OS_TYPE" != "Darwin" ]; then
log "Detected CPU: ${CPU_MODEL:-unknown} — using XMRig auto-tuning (threads, asm, MSR, NUMA auto-detected)."
# ISA preflight (#338): surface missing AES-NI/AVX2 here, at the moment the rig is being
# configured, instead of letting unsupported hardware mine silently slow. Warn, never abort:
# XMRig still runs (soft AES / non-AVX2 init) and a knowingly-old rig is a valid choice.
local _missing_isa
_missing_isa=$(_cpu_missing_isa)
[[ " $_missing_isa " == *" aes "* ]] && warn "This CPU has no AES-NI: RandomX falls back to soft AES, roughly 4x slower. Mining will work, but expect a fraction of a modern CPU's rate."
[[ " $_missing_isa " == *" avx2 "* ]] && warn "This CPU has no AVX2: dataset init will be slower (steady-state hashrate is unaffected)."
fi

# Rig label for the pool `user` field (#22): any pool entry that didn't set its own `user` gets the
Expand Down Expand Up @@ -4385,6 +4408,22 @@ _lockdown_state() { # -> none|integrity|confidentiality, or empty when unknown
return 0
}

# RandomX ISA preflight (#338, the last acceptance criterion from #1): echo which of aes / avx2 the
# CPU lacks, from the kernel's flags line. RandomX without AES-NI silently falls back to XMRig's
# soft-AES path (~4x slower) and without AVX2 dataset init slows — neither aborts anything, so a rig
# on unsupported hardware "works" at a mysteriously bad rate unless somebody says why. Judged ONLY
# when an x86-style "flags" line exists: no flags line (macOS has no /proc, ARM cpuinfo says
# "Features", sandboxes stub the file) means unknown, and unknown never manufactures an issue — the
# same stance as #333's lockdown probe. `-w` so vaes/avx2_vnni style neighbors can't false-match.
_cpu_missing_isa() { # -> "aes", "avx2", "aes avx2", or empty when all present / undeterminable
grep -q '^flags' "$CPUINFO" 2>/dev/null || return 0
local missing=""
grep -qw aes "$CPUINFO" || missing="aes"
grep -qw avx2 "$CPUINFO" || missing="$missing${missing:+ }avx2"
printf '%s' "$missing"
return 0
}

# True when lockdown is at a level that blocks MSR writes (#333). LOCKDOWN_MSR sits below
# LOCKDOWN_INTEGRITY_MAX in enum lockdown_reason (include/linux/security.h), so both `integrity` and
# `confidentiality` deny the write; only `none` permits it.
Expand Down Expand Up @@ -4746,6 +4785,21 @@ doctor() {
_ck_warn "1GB HugePages not reserved (optional; needs a pdpe1gb CPU + reboot)"
fi

# CPU ISA support (#338): a rig without AES-NI mines at soft-AES speed (~4x slower) with no
# error anywhere — exactly the "silently failing" #1's acceptance criterion forbids, so it's a
# counted issue. Missing AVX2 only slows dataset init: advisory. Quiet when there's no x86
# flags line to judge (unknown, not unsupported).
local miss_isa
miss_isa=$(_cpu_missing_isa)
if [[ " $miss_isa " == *" aes "* ]]; then
_ck_warn "CPU has no AES-NI — RandomX runs soft AES, roughly 4x slower; this hardware cannot mine at a competitive rate"
issues=$((issues + 1))
else
# The ok line only when there IS a flags line to have judged; no flags line = unknown, say nothing.
grep -q '^flags' "$CPUINFO" 2>/dev/null && _ck_ok "CPU supports AES-NI (hardware RandomX path)" || true
fi
[[ " $miss_isa " == *" avx2 "* ]] && _ck_info "CPU has no AVX2 — dataset init is slower (steady-state hashrate unaffected)"

# Resolve the worker's xmrig.log once — the MSR-applied (#66) and HUGE PAGES checks both read it.
local wr="" log_file=""
if [ -f "$CONFIG_JSON" ]; then
Expand Down
1 change: 1 addition & 0 deletions tests/perf-baselines/miner-0.history.jsonl
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
{"tag":"v1.10.0","recorded":"2026-07-17","bench_1m_hs":10736.5}
{"tag":"v1.11.1","recorded":"2026-07-18","bench_1m_hs":10743.9}
{"tag":"v1.13.0","recorded":"2026-08-01","bench_1m_hs":10733.4}
{"tag":"v1.13.1","recorded":"2026-08-01","bench_1m_hs":10762.5}
2 changes: 1 addition & 1 deletion tests/perf-baselines/miner-0.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"bench_1m_hs": 10733.4,
"bench_1m_hs": 10762.5,
"cpu": "AMD Ryzen 7 7800X3D 8-Core Processor",
"recorded": "2026-08-01"
}
121 changes: 121 additions & 0 deletions tests/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2534,6 +2534,56 @@ chmod +x "$LT"/*
PATH="$LT" CALL_LOG="$LT/calls.log" install_dependencies </dev/null
) >/dev/null 2>&1
assert_contains "apt install list includes linux-tools-<rel> (#74)" "$(cat "$LT/calls.log")" "linux-tools-6.0.0-rig"
# The same run doubles as the Ubuntu cpupower shape (#327): apt-cache says linux-tools-common
# exists, so it's picked and the Debian name never enters the list.
assert_contains "Ubuntu shape: cpupower via linux-tools-common (#327)" "$(cat "$LT/calls.log")" "linux-tools-common"
assert_absent "Ubuntu shape: linux-cpupower stays out (#327)" "$(cat "$LT/calls.log")" "linux-cpupower"

# #327: cpupower's apt package is distro-dependent — linux-tools-common (Ubuntu) vs linux-cpupower
# (Debian) — and apt's all-or-nothing transaction means one unknown name kills the ENTIRE dependency
# install (gcc/cmake included). The probe must pick the name the distro ships, and a double miss must
# warn without touching the toolchain install. Ubuntu shape is asserted on the #74 run above.
echo "== unit: apt cpupower package probe — Debian / neither exists (#327) =="
DEB="$(mktemp -d "$SANDBOX/deb327.XXXXXX")"
printf '#!/bin/sh\nexit 1\n' >"$DEB/dpkg" # every dep "missing" -> all go to the install list
cat >"$DEB/apt-cache" <<'EOF'
#!/bin/sh
# Debian trixie shape: linux-cpupower exists; linux-tools-common and linux-tools-<rel> do not.
case "$*" in *linux-cpupower*) exit 0 ;; *) exit 1 ;; esac
EOF
printf '#!/bin/sh\necho "[apt-get] $*" >>"$CALL_LOG"\n' >"$DEB/apt-get"
printf '#!/bin/sh\nwhile [ "${1#*=}" != "$1" ]; do export "$1"; shift; done\nexec "$@"\n' >"$DEB/sudo"
printf '#!/bin/sh\necho 6.0.0-rig\n' >"$DEB/uname"
chmod +x "$DEB"/*
: >"$DEB/calls.log"
(
source "$SCRIPT"
OS_TYPE=Linux REAL_USER=test
PATH="$DEB" CALL_LOG="$DEB/calls.log" install_dependencies </dev/null
) >/dev/null 2>&1
assert_contains "Debian shape: cpupower via linux-cpupower (#327)" "$(cat "$DEB/calls.log")" "linux-cpupower"
assert_absent "Debian shape: linux-tools-common stays out (#327)" "$(cat "$DEB/calls.log")" "linux-tools-common"

# Neither name exists (apt-cache always says no): warn, keep going, and the toolchain still installs.
NC="$(mktemp -d "$SANDBOX/nc327.XXXXXX")"
printf '#!/bin/sh\nexit 1\n' >"$NC/dpkg"
printf '#!/bin/sh\nexit 1\n' >"$NC/apt-cache" # no cpupower package under ANY name
printf '#!/bin/sh\necho "[apt-get] $*" >>"$CALL_LOG"\n' >"$NC/apt-get"
printf '#!/bin/sh\nwhile [ "${1#*=}" != "$1" ]; do export "$1"; shift; done\nexec "$@"\n' >"$NC/sudo"
printf '#!/bin/sh\necho 6.0.0-rig\n' >"$NC/uname"
chmod +x "$NC"/*
: >"$NC/calls.log"
nc_out="$( (
source "$SCRIPT"
OS_TYPE=Linux REAL_USER=test
PATH="$NC" CALL_LOG="$NC/calls.log" install_dependencies </dev/null
) 2>&1)"
rc=$?
assert_rc "no cpupower package never fails the install (#327)" "$rc" "0"
assert_contains "warns when no cpupower package exists (#327)" "$nc_out" "No cpupower package found"
assert_contains "toolchain still installs without a cpupower package (#327)" "$(cat "$NC/calls.log")" "build-essential"
assert_absent "no cpupower name reaches apt when neither exists (#327)" "$(cat "$NC/calls.log")" "linux-tools-common"
assert_absent "linux-cpupower also stays out when absent (#327)" "$(cat "$NC/calls.log")" "linux-cpupower"

# check_prerequisites (the jq bootstrap) had NO test. jq is deliberately kept OFF the scenario PATH so the
# install branch runs; each dir holds ONLY the package manager(s) under test, so `command -v` selects the
Expand Down Expand Up @@ -3418,6 +3468,77 @@ assert_contains "bios menu: Gigabyte Secure Boot path (#333)" "$(bm_sb "Gigabyte
assert_contains "bios menu: MSI Secure Boot path (#333)" "$(bm_sb "Micro-Star International")" "Windows OS Configuration"
assert_contains "bios menu: unknown vendor falls back generically (#333)" "$(bm_sb "Some OEM")" "usually under Boot or Security"

# #338 (the last #1 acceptance criterion): missing AES-NI/AVX2 must be SURFACED — soft-AES mining is
# ~4x slower with no error anywhere. AES-NI missing = counted doctor issue + setup warn; AVX2 missing =
# advisory only; no x86 "flags" line (macOS, ARM's "Features", stubs) = unknown = silence, never an issue.
echo "== unit: CPU ISA preflight — AES-NI / AVX2 surfaced (#338) =="
printf 'processor : 0\nflags : fpu vme aes avx avx2 vaes\n' >"$DOC/cpuinfo_full"
# vaes but NOT the standalone aes word: proves the -w match can't be satisfied by a neighbor flag.
printf 'processor : 0\nflags : fpu vme avx avx2 vaes\n' >"$DOC/cpuinfo_noaes"
printf 'processor : 0\nflags : fpu vme aes avx\n' >"$DOC/cpuinfo_noavx2"
printf 'processor : 0\nflags : fpu vme avx\n' >"$DOC/cpuinfo_neither"
printf 'processor : 0\nFeatures : fp asimd aes\n' >"$DOC/cpuinfo_arm" # ARM shape: no "flags" line

# --- the pure helper, exercised directly ---
isa_miss() { (source "$SCRIPT" && CPUINFO="$1" _cpu_missing_isa); }
assert_eq "isa: full flags -> nothing missing (#338)" "$(isa_miss "$DOC/cpuinfo_full")" ""
assert_eq "isa: vaes does not satisfy the aes word-match (#338)" "$(isa_miss "$DOC/cpuinfo_noaes")" "aes"
assert_eq "isa: missing avx2 reported alone (#338)" "$(isa_miss "$DOC/cpuinfo_noavx2")" "avx2"
assert_eq "isa: both missing, space-separated (#338)" "$(isa_miss "$DOC/cpuinfo_neither")" "aes avx2"
assert_eq "isa: ARM Features line -> unknown, not unsupported (#338)" "$(isa_miss "$DOC/cpuinfo_arm")" ""
assert_eq "isa: absent cpuinfo -> unknown (#338)" "$(isa_miss "/nonexistent-cpuinfo")" ""

# --- doctor: counted for aes, advisory for avx2, silent on unknown ---
out="$(CPUINFO="$DOC/cpuinfo_noaes" run_doctor "$DOC/meminfo_ok" "$DOC/msrmod" "$DOC/gov_perf" "$DOC/nr1g")"
assert_contains "doctor: missing AES-NI named (#338)" "$out" "CPU has no AES-NI"
assert_contains "doctor: missing AES-NI is a counted issue (#338)" "$out" "issue(s) found"
out="$(CPUINFO="$DOC/cpuinfo_noavx2" run_doctor "$DOC/meminfo_ok" "$DOC/msrmod" "$DOC/gov_perf" "$DOC/nr1g")"
assert_contains "doctor: missing AVX2 is advisory (#338)" "$out" "CPU has no AVX2"
assert_contains "doctor: missing AVX2 alone still passes (#338)" "$out" "all critical checks passed"
out="$(CPUINFO="$DOC/cpuinfo_full" run_doctor "$DOC/meminfo_ok" "$DOC/msrmod" "$DOC/gov_perf" "$DOC/nr1g")"
assert_contains "doctor: AES-NI present reported ok (#338)" "$out" "CPU supports AES-NI"
out="$(CPUINFO="$DOC/cpuinfo_arm" run_doctor "$DOC/meminfo_ok" "$DOC/msrmod" "$DOC/gov_perf" "$DOC/nr1g")"
assert_absent "doctor: unknown ISA raises no alarm (#338)" "$out" "AES-NI"

# --- setup path: generate_xmrig_config warns at configure time, and never aborts ---
export STUB_CPU_MODEL="Old Xeon E5405" STUB_NPROC=4 STUB_HOSTNAME=rigbox
ISA338="$(mktemp -d "$SANDBOX/isa338.XXXXXX")"
gen338_out="$(
cd "$ISA338" || exit 1
source "$SCRIPT"
OS_TYPE=Linux
WORKER_ROOT="$ISA338"
POOL_ADDRESS=myrig.local
POOLS_JSON='[{"url":"myrig.local:3333","user":"","pass":"x","keepalive":true,"tls":false,"enabled":true}]'
ACCESS_TOKEN=tok123
DONATION=1
LOGROTATE_DIR="$ISA338"
CPUINFO="$DOC/cpuinfo_neither"
set +e
PATH="$STUBS:$PATH" generate_xmrig_config 2>&1
)"
assert_rc "config-gen still succeeds on unsupported hardware (#338)" "$?" "0"
assert_contains "config-gen warns about missing AES-NI (#338)" "$gen338_out" "no AES-NI"
assert_contains "config-gen warns about missing AVX2 (#338)" "$gen338_out" "no AVX2"
assert_contains "config-gen: the config was still generated (#338)" "$(J "$ISA338/config.json" '.pools[0].url')" "myrig.local:3333"
# A fully-capable CPU stays quiet — the warn must not become noise on normal rigs.
QUIET338="$(mktemp -d "$SANDBOX/isaq338.XXXXXX")"
genq_out="$(
cd "$QUIET338" || exit 1
source "$SCRIPT"
OS_TYPE=Linux
WORKER_ROOT="$QUIET338"
POOL_ADDRESS=myrig.local
POOLS_JSON='[{"url":"myrig.local:3333","user":"","pass":"x","keepalive":true,"tls":false,"enabled":true}]'
ACCESS_TOKEN=tok123
DONATION=1
LOGROTATE_DIR="$QUIET338"
CPUINFO="$DOC/cpuinfo_full"
set +e
PATH="$STUBS:$PATH" generate_xmrig_config 2>&1
)"
assert_absent "config-gen: no ISA warning on a capable CPU (#338)" "$genq_out" "AES-NI"

# #278: doctor reports control receiver health when `control` is enabled. Active + responding (200 or
# 503, per util/control-server.py) is ok; enabled-but-down (service inactive, or active but not
# answering) warns with a hint and counts as an issue; disabled prints no control-receiver lines at all.
Expand Down