igneum/packaging/linux/selftest.sh
igneum-labs 79bf032ecb Rig: OTA key list with revocation (the app's ota-k2 form), prover gate at the 24 GB tier, prepare-ahead documented
packaging/linux/bin/igneum-rig-lib.sh: OTA_PUBLIC_KEYS (K1, the empty K2 slot), manifest_check = the app's manifest::check (ota-k2 063b988): every embedded key not revoked is tried and the signer remembered, the manifest's revoked_keys except the signer's own are recorded in /var/lib/igneum/updates/revoked.json (igneum-revoked-keys/1), a signature only a revoked key verifies is refused as "manifest signature is by a revoked key (sha256:<8 hex>)", a corrupt record reads as empty; the installer and the hourly updater go through it. selftest.sh: the app's revocation_path cases on three throwaway keys. Prover gates from provedefault.rs at 440fd59 (bench-log "proving v1", the S_p curve): PROVER_MIN_VRAM_MB and PROVER_MINE_AND_PROVE_MB 23,552; the README table states each tier's consequence. README: the miner takes the prepare-ahead path (both shipped workers answer prepare 1; exit 42 only without prepare support or on a seed-mismatch fault), so a 10-minute epoch costs no restart. igneum-node.sh and selftest.sh without mapfile (bash 3.2 on the Mac).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 20:46:18 +00:00

147 lines
13 KiB
Bash
Executable file

#!/usr/bin/env bash
# Self-test of the rig package on a machine that is not the rig (the Mac, 5 October 2026): bash -n and shellcheck on
# every script, the unit files through check-units.sh, the card inventory on a fake sysfs tree (NVIDIA, AMD, Intel,
# an AMD APU, an Intel iGPU and an ASPEED BMC VGA, the last three excluded), the OpenCL index mapping on a fake
# --list, the prover default rule and the identities rule on fake inventories, the Ed25519 check against the LIVE
# signed manifest (and a tampered copy refused), then the installer's --dry-run end to end, which downloads and
# verifies the live package into a scratch folder. Nothing under / is touched. Needs network for the last two.
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
pass() { printf ' ok %s\n' "$*"; }
fail() { printf ' FAIL %s\n' "$*"; exit 1; }
echo "== bash -n and shellcheck"
for f in "$HERE"/install-rig.sh "$HERE"/selftest.sh "$HERE"/check-units.sh "$HERE"/bin/*; do bash -n "$f" || fail "bash -n $f"; done
pass "bash -n on $(find "$HERE/bin" -type f | wc -l | tr -d ' ') scripts plus the installer, the checker and this test"
if command -v shellcheck >/dev/null 2>&1; then
shellcheck -x -S style "$HERE"/install-rig.sh "$HERE"/selftest.sh "$HERE"/check-units.sh "$HERE"/bin/* || fail "shellcheck"
pass "shellcheck $(shellcheck --version | awk '/version:/ {print $2}') clean at -S style"
else echo " note shellcheck is not installed (brew install shellcheck); skipped"; fi
echo "== unit files"
"$HERE/check-units.sh" || fail "check-units.sh"
echo "== card inventory on a fake sysfs tree"
S="$T/sys"; P="$S/bus/pci/devices"; mkdir -p "$P" "$S/bus/pci/drivers/amdgpu" "$S/bus/pci/drivers/xe"
mk() { # <bus> <class> <vendor> <device> [vram bytes] [product]
mkdir -p "$P/$1"; printf '%s\n' "$2" > "$P/$1/class"; printf '%s\n' "$3" > "$P/$1/vendor"; printf '%s\n' "$4" > "$P/$1/device"
[[ -n "${5:-}" ]] && printf '%s\n' "$5" > "$P/$1/mem_info_vram_total"; [[ -n "${6:-}" ]] && printf '%s\n' "$6" > "$P/$1/product_name"; true
}
mk 0000:01:00.0 0x030000 0x10de 0x2b85 # RTX 5090
mk 0000:21:00.0 0x030000 0x10de 0x2b85 # RTX 5090
mk 0000:41:00.0 0x030000 0x1002 0x7550 17163091968 "AMD Radeon RX 9070 XT"
mk 0000:42:00.0 0x030000 0x1002 0x7550 17163091968 "AMD Radeon RX 9070 XT"
mk 0000:61:00.0 0x030000 0x8086 0xe20b # Arc B580
mk 0000:00:02.0 0x030000 0x8086 0x7d55 # Intel iGPU, excluded
mk 0000:0c:00.0 0x030000 0x1002 0x164e 536870912 "AMD Radeon Graphics" # APU, excluded
mk 0000:03:00.0 0x030000 0x1a03 0x2000 # ASPEED BMC VGA, excluded
mk 0000:05:00.0 0x020000 0x8086 0x1521 # a NIC, not a display device
inv="$(IGNEUM_SYS_ROOT="$S" "$HERE/bin/igneum-gpus.sh" 2> "$T/notes")"
printf '%s\n' "$inv" | sed 's/^/ card /'; sed 's/^/ note /' "$T/notes"
[[ "$(printf '%s\n' "$inv" | wc -l | tr -d ' ')" == 5 ]] || fail "expected 5 cards, got: $inv"
printf '%s\n' "$inv" | grep -q '^nvidia1 nvidia 0000:21:00.0 0 1 cuda NVIDIA 0x2b85$' || fail "nvidia1 line"
printf '%s\n' "$inv" | grep -q '^amd1 amd 0000:42:00.0 16368 1 opencl AMD Radeon RX 9070 XT$' || fail "amd1 line"
printf '%s\n' "$inv" | grep -q '^intel0 intel 0000:61:00.0 0 0 opencl Intel 0xe20b$' || fail "intel0 line"
if ! { grep -q 'skip 0000:00:02.0: Intel integrated' "$T/notes" && grep -q 'skip 0000:0c:00.0: AMD integrated' "$T/notes" && grep -q 'skip 0000:03:00.0: display device of vendor 0x1a03' "$T/notes"; }; then fail "exclusions"; fi
pass "5 cards in PCI order per vendor; the iGPU, the APU and the BMC VGA excluded; the NIC ignored"
echo "== library rules on the fake inventory"
# shellcheck source=bin/igneum-rig-lib.sh
. "$HERE/bin/igneum-rig-lib.sh"
IGNEUM_ROOT="$T/root"; mkdir -p "$IGNEUM_ROOT/bin"; cp "$HERE/bin/igneum-gpus.sh" "$IGNEUM_ROOT/bin/"
export IGNEUM_SYS_ROOT="$S"
IGNEUM_ETC="$T/etc"; mkdir -p "$IGNEUM_ETC"; RIG_CONF="$IGNEUM_ETC/rig.conf"
printf 'WALLET=0xdfaea6000000000000000000000000000000002c2e\nRIG_NAME=rig1\nNETWORK=devnet\n' > "$RIG_CONF"
load_conf
[[ "$(card_identities nvidia0)" == 8 && "$(card_identities amd0)" == 8 ]] || fail "identities auto: big cards get 8"
mk 0000:22:00.0 0x030000 0x10de 0x2882 ; nv_small="$(IGNEUM_SYS_ROOT="$S" "$HERE/bin/igneum-gpus.sh" 2>/dev/null | grep -c '^nvidia')"; [[ "$nv_small" == 3 ]] || fail "third NVIDIA card"
pass "identities: auto gives 8 to a card of 8 GiB or more (and to one whose VRAM is unknown), as the app's rule"
printf 'IDENTITIES=2\n' >> "$RIG_CONF"; load_conf; [[ "$(card_identities nvidia0)" == 2 ]] || fail "IDENTITIES=2 applies"; pass "IDENTITIES=2 applies to every card"
[[ "$(card_labels_with_identities amd1 | tr '\n' ' ')" == "rig1-amd1-1 rig1-amd1-2 " ]] || fail "labels"; pass "vote-key labels rig1-amd1-1, rig1-amd1-2 (the miner's <label>-<i> rule)"
# the prover rule: no VRAM known for the fake NVIDIA cards (nvidia-smi absent) -> off by default; PROVER=on picks one
d="$(prover_decision)"; [[ "$d" == off* ]] || fail "prover default with unknown VRAM: $d"
printf 'PROVER=on\n' >> "$RIG_CONF"; load_conf; d="$(prover_decision)"; [[ "$d" == "on nvidia0 pause PROVER=on"* ]] || fail "PROVER=on: $d"
pass "prover: off by default when no NVIDIA VRAM is known; PROVER=on takes nvidia0 and pauses its miner under the 20,480 MB line"
rm -r "$P/0000:22:00.0"
printf 'PROVER=auto\nPROVER_PAUSE_MINER=never\n' >> "$RIG_CONF"; load_conf; [[ "$(prover_decision)" == off* ]] || fail "auto stays off"
pass "prover_decision output shape: $(prover_decision | cut -c1-60)..."
echo "== OpenCL index mapping on a fake --list"
fake_list="$(cat <<'L'
[0] gfx1201 | AMD Accelerated Parallel Processing (OpenCL 2.1 AMD-APP (3649.0))
GPU, vendor Advanced Micro Devices, Inc., driver 3649.0, OpenCL C 2.0, 64 compute units, 2970 MHz
[1] gfx1201 | AMD Accelerated Parallel Processing (OpenCL 2.1 AMD-APP (3649.0))
GPU, vendor Advanced Micro Devices, Inc., driver 3649.0, OpenCL C 2.0, 64 compute units, 2970 MHz
[2] Intel(R) Arc(TM) B580 Graphics | Intel(R) OpenCL Graphics (OpenCL 3.0)
GPU, vendor Intel(R) Corporation, driver 25.05.32567, OpenCL C 1.2, 160 compute units, 2850 MHz
[3] Intel(R) Arc(TM) B580 Graphics | Intel(R) OpenCL Graphics (OpenCL 3.0)
GPU, vendor Intel(R) Corporation, driver 25.05.32567, OpenCL C 1.2, 160 compute units, 2850 MHz
L
)"
[[ "$(printf '%s\n' "$fake_list" | opencl_index_for amd 1)" == 1 && "$(printf '%s\n' "$fake_list" | opencl_index_for intel 0)" == 2 && "$(printf '%s\n' "$fake_list" | opencl_index_for intel 1)" == 3 && -z "$(printf '%s\n' "$fake_list" | opencl_index_for amd 2)" ]] || fail "opencl_index_for"
pass "amd1 -> OpenCL 1, intel0 -> 2, intel1 -> 3, amd2 -> none"
echo "== the live signed manifest (Ed25519 with the OTA public key)"
unset IGNEUM_SYS_ROOT
fetch_manifest "$T/live" || fail "fetch_manifest"
manifest="$T/live/igneum-app-latest.json"
pass "signature verifies ($MANIFEST_VERIFIER): version $(manifest_field "$manifest" 'm.get("version")'), override $(manifest_override "$manifest")"
cp "$manifest" "$T/live/tampered.json"; printf ' ' >> "$T/live/tampered.json"
if verify_manifest_signature "$T/live/tampered.json" "$manifest.sig"; then fail "a tampered manifest verified"; else pass "a tampered manifest is refused"; fi
printf '%s' "$(tr -d '[:space:]' < "$manifest.sig" | sed 's/^./0/')" > "$T/live/bad.sig"
if verify_manifest_signature "$manifest" "$T/live/bad.sig"; then fail "a bad signature verified"; else pass "a bad signature is refused"; fi
if [[ -z "$(manifest_package "$manifest")" ]]; then pass "the manifest names no linux package today (sidecar mode needed, as documented)"; else pass "the manifest names a linux package: $(manifest_package "$manifest")"; fi
echo "== key list and revocation (throwaway keys; the app's manifest::tests::revocation_path)"
K="$T/keys"; mkdir -p "$K"
python3 - "$K" <<'PYKEYS' || fail "throwaway keys need python3 cryptography"
import json, sys, hashlib
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from cryptography.hazmat.primitives import serialization
d = sys.argv[1]; raw = serialization.Encoding.Raw; pub = serialization.PublicFormat.Raw
ks = [Ed25519PrivateKey.from_private_bytes(bytes([i] * 32)) for i in (1, 2, 3)]
pubs = [k.public_key().public_bytes(raw, pub).hex() for k in ks]
open(d + "/pubs", "w").write("\n".join(pubs) + "\n")
fps = [hashlib.sha256(bytes.fromhex(p)).hexdigest() for p in pubs]
open(d + "/fps", "w").write("\n".join(fps) + "\n")
def sign(name, signer, body):
data = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
open(d + "/" + name, "wb").write(data); open(d + "/" + name + ".sig", "w").write(ks[signer].sign(data).hex())
base = {"version": "0.9.9", "channel": "test", "platforms": {}}
sign("k1.json", 0, base) # a plain K1 manifest
sign("k2-revokes-k1.json", 1, dict(base, revoked_keys=[fps[0]])) # K2 revokes K1
sign("k1-self.json", 0, dict(base, revoked_keys=[fps[0]])) # K1 lists itself: ignored
sign("k3.json", 2, base) # an unknown key
sign("k1-revokes-k2.json", 0, dict(base, revoked_keys=[fps[1]])) # a dead K1 tries to revoke K2 back
PYKEYS
PUBS=(); FPS=(); while read -r l; do PUBS+=("$l"); done < "$K/pubs"; while read -r l; do FPS+=("$l"); done < "$K/fps"
OTA_PUBLIC_KEYS=("${PUBS[0]}" "" "${PUBS[1]}") # K1, an empty slot, K2
REV="$K/revoked.json"
manifest_check "$K/k1.json" "$K/k1.json.sig" "$REV" || fail "a K1 manifest verifies"; [[ "$MANIFEST_SIGNER_FP8" == "${FPS[0]:0:8}" && ! -f "$REV" ]] || fail "K1 named, no record written"
pass "a K1 manifest verifies (signer sha256:$MANIFEST_SIGNER_FP8, $MANIFEST_VERIFIER), the empty slot skipped, nothing recorded"
manifest_check "$K/k3.json" "$K/k3.json.sig" "$REV" 2>/dev/null && fail "an unknown key verified"; pass "an unknown key's manifest is refused"
manifest_check "$K/k1-self.json" "$K/k1-self.json.sig" "$REV" || fail "K1 listing itself"; [[ -z "$MANIFEST_NEWLY_REVOKED" && ! -f "$REV" ]] || fail "a key must never revoke itself"
pass "K1 listing its own fingerprint is ignored (a manifest can never leave the rig with no trusted key)"
manifest_check "$K/k2-revokes-k1.json" "$K/k2-revokes-k1.json.sig" "$REV" || fail "K2 revoking K1"; [[ "$MANIFEST_NEWLY_REVOKED" == "${FPS[0]}" ]] || fail "K1's fingerprint recorded"
python3 -c 'import json,sys; r=json.load(open(sys.argv[1])); assert r["format"]=="igneum-revoked-keys/1" and r["revoked"][0]["fingerprint"]==sys.argv[2] and r["revoked"][0]["by"]==sys.argv[3] and r["revoked"][0]["manifest_version"]=="0.9.9" and r["revoked"][0]["at"]>0, r' "$REV" "${FPS[0]}" "${FPS[1]}" || fail "revoked.json shape"
pass "a K2 manifest listing K1 records it in revoked.json (fingerprint, by, manifest_version, at)"
err="$(manifest_check "$K/k1.json" "$K/k1.json.sig" "$REV" 2>&1 >/dev/null)" && fail "a revoked K1 verified"; [[ "$err" == *"manifest signature is by a revoked key (sha256:${FPS[0]:0:8})"* ]] || fail "revoked key named: $err"
pass "K1 is then refused by name: ${err#* WARNING: }"
err="$(manifest_check "$K/k1-revokes-k2.json" "$K/k1-revokes-k2.json.sig" "$REV" 2>&1 >/dev/null)" && fail "a dead K1 revoked K2"; manifest_check "$K/k2-revokes-k1.json" "$K/k2-revokes-k1.json.sig" "$REV" || fail "K2 still verifies"
[[ "$(python3 -c 'import json,sys; print(len(json.load(open(sys.argv[1]))["revoked"]))' "$REV")" == 1 ]] || fail "no duplicate record"
pass "a dead K1 cannot revoke K2 back; K2 still verifies; the record is not duplicated"
printf 'garbage' > "$REV"; manifest_check "$K/k1.json" "$K/k1.json.sig" "$REV" || fail "corrupt record reads as empty"; pass "a corrupt revoked.json reads as empty (the next revoking manifest writes it again)"
OTA_PUBLIC_KEYS=("$OTA_PUBLIC_KEY_HEX" "$OTA_PUBLIC_KEY_2_HEX")
echo "== install-rig.sh --dry-run (downloads and verifies the live package into a scratch folder)"
IGNEUM_ROOT="$T/opt" IGNEUM_ETC="$T/etc2" IGNEUM_VAR="$T/var" IGNEUM_RUN="$T/run" "$HERE/install-rig.sh" --dry-run --yes --wallet 0xDFAEA60000000000000000000000000000002C2E --name "rig 1" --allow-sidecar-sha256 --prover auto > "$T/dry.out" 2>&1 || { cat "$T/dry.out"; fail "dry run exited non-zero"; }
grep -q 'downloaded and verified: igneum-hive-' "$T/dry.out" || { cat "$T/dry.out"; fail "no verified download in the dry run"; }
grep -q '\.sha256 sidecar agrees' "$T/dry.out" || fail "sidecar check"
if ! { grep -q 'bin/igneumd .* bytes' "$T/dry.out" && grep -q 'bin/igneum-worker-opencl .* bytes' "$T/dry.out"; }; then fail "package contents"; fi
grep -q '\[dry-run\] systemctl enable igneum-node.service' "$T/dry.out" || fail "enable step"
if ! { grep -q 'WALLET=0xdfaea600' "$T/dry.out" && grep -q 'RIG_NAME=rig-1' "$T/dry.out"; }; then fail "config step (wallet lower-cased, name sanitised)"; fi
grep -c '^\[dry-run\]' "$T/dry.out" | sed 's/^/ dry-run steps printed: /'
grep -E 'glibc igneumd|glibc igneum-miner' "$T/dry.out" | sed 's/^ */ /'
pass "dry run: preflight (fails as expected off the rig), manifest, download, verification, config, units, enable, start all printed"
echo "== self-test passed (scripts, units, inventory, rules, the live manifest and package; the rig itself is untested)"