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
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ Pithead ships as **one product, one version** — the version lives in the top-l
[`VERSION`](VERSION) file and every released image is tagged with it. Releases are cut
per the process in [`docs/dev/releasing.md`](docs/dev/releasing.md).

## [1.10.2] - 2026-07-21

### Fixed

- **The payout wallet no longer reports the whole stack unhealthy during its first scan (#718).**
With the genesis-scan default, `wallet-rpc`'s initial full-chain scan takes hours, and
monero-wallet-rpc doesn't answer its RPC while scanning — so the healthcheck flipped `unhealthy`
and could fire stack-health alerts for the entire scan, even though the wallet was working. The
healthcheck now tolerates an unreachable RPC while a first-scan marker is present (armed on wallet
creation, cleared the first time the RPC answers), so it stays healthy through the initial scan
and turns strict once caught up.
- **Control-gate refusal messages no longer cite a closed, unrelated issue (#713).** The
"security-sensitive setting" and "destructive change" refusals pointed operators at #338 (a
closed Telegram-control issue) for "out-of-band approval"; they now just name the real path —
edit `config.json` on the host and run `./pithead apply`.

## [1.10.1] - 2026-07-21

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.10.1
1.10.2
2 changes: 1 addition & 1 deletion build/dashboard/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "mining-dashboard"
# Keep in lockstep with the top-level VERSION file — the single source of truth for the stack version
# (#44). A shell test (tests/stack/run.sh) fails if these drift; the dashboard *displays* the version
# from VERSION (baked in as PITHEAD_VERSION, #58), so this is packaging metadata only.
version = "1.10.1"
version = "1.10.2"
description = "Monitoring dashboard and XvB switching engine for Pithead"
readme = "README.md"
requires-python = ">=3.11"
Expand Down
2 changes: 1 addition & 1 deletion build/dashboard/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions build/monero/wallet-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ set -eu

WALLET_DIR="${WALLET_DIR:-/home/ubuntu/wallets}"
WALLET_FILE="$WALLET_DIR/payout-wallet"
# Marker (#718): touched when a wallet is first created, cleared by the healthcheck on the first
# successful RPC. While it exists, an unreachable RPC means "still on the initial scan" — which for
# the genesis default is HOURS, during which monero-wallet-rpc is single-threaded and won't answer.
# It lives in the volume so it persists across container recreates until the scan actually finishes.
SCAN_MARKER="$WALLET_DIR/.payout-scanning"
GEN_JSON="${GEN_JSON:-/tmp/gen.json}" # tmpfs; holds the view key for the create-from-keys step only
DAEMON_ADDRESS="${MONERO_NODE_HOST:-127.0.0.1}:${MONERO_RPC_PORT:-18081}"

Expand Down Expand Up @@ -76,6 +81,9 @@ if [ ! -f "$WALLET_FILE" ]; then
height="$(resolve_scan_height)"
[ -n "$height" ] || height=0
echo "Creating view-only payout wallet at restore height $height (#381)..."
# Mark the initial scan (#718): the healthcheck tolerates an unreachable RPC while this exists
# (genesis scan is hours) and clears it on the first successful RPC.
touch "$SCAN_MARKER" 2>/dev/null || true
# The view key lives ONLY in this tmpfs file, never on argv.
write_gen_json "$height"
# --generate-from-json creates + opens the wallet, then keeps serving the RPC.
Expand Down
47 changes: 34 additions & 13 deletions build/monero/wallet-healthcheck.sh
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
#!/bin/sh
# monero-wallet-rpc liveness check (#381).
# monero-wallet-rpc health (#381, #718).
#
# Reads the RPC credentials from the container's environment instead of taking them as arguments,
# so they are NOT baked into the compose `healthcheck.test` — where `docker inspect` could read
# them (#90). A JSON-RPC `get_version` proves the server is up and answering; it succeeds during
# the initial scan too, so the long start_period in compose tolerates first-run blockchain scanning
# without flapping unhealthy.
set -eu
# RPC-up is the normal signal: a JSON-RPC get_version proves the server is answering. Credentials
# come from the environment, not argv, so `docker inspect` can't read them (#90).
#
# But during the INITIAL payout scan (#718) monero-wallet-rpc is single-threaded and heads-down for
# the whole scan — with the genesis default it is HOURS, and it refuses the RPC the entire time. A
# plain RPC check flaps unhealthy after the 2m start_period and spams stack-health alerts for the
# whole first scan. So: a marker file (`.payout-scanning`, written by the entrypoint on wallet
# creation, living in the volume so it survives recreates) means "still on the first scan" — while
# it exists, an unreachable RPC is treated as healthy. The first time the RPC answers, the scan has
# caught up, so we clear the marker and are strict from then on (a later RPC failure is a real
# fault, not scan tolerance).
set -u

WALLET_DIR="${WALLET_DIR:-/home/ubuntu/wallets}"
SCAN_MARKER="$WALLET_DIR/.payout-scanning"

rpc_up() {
curl -fsS --digest \
-u "${WALLET_RPC_USERNAME:-wallet}:${WALLET_RPC_PASSWORD:-}" \
-o /dev/null \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
http://localhost:18082/json_rpc
}

if rpc_up; then
# Caught up and answering — retire the initial-scan grace so future RPC failures read as real.
rm -f "$SCAN_MARKER" 2>/dev/null || true
exit 0
fi

curl -fsS --digest \
-u "${WALLET_RPC_USERNAME:-wallet}:${WALLET_RPC_PASSWORD:-}" \
-o /dev/null \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"get_version"}' \
http://localhost:18082/json_rpc
# RPC silent. Healthy only if we're still on the first scan (marker present); else it's a fault.
[ -f "$SCAN_MARKER" ] && exit 0
exit 1
4 changes: 2 additions & 2 deletions pithead
Original file line number Diff line number Diff line change
Expand Up @@ -5075,11 +5075,11 @@ control_approval_gate() { # <staged-file>
bad=$(printf '%s' "$porcelain" | awk -F'\t' 'NF' | cut -f2 | grep -cvxE "$editable_re" || true)
if [ "${bad:-0}" -gt 0 ]; then
hit=$(printf '%s' "$porcelain" | awk -F'\t' 'NF' | cut -f2 | grep -m1 -vxE "$editable_re" || true)
printf 'this change alters a security-sensitive setting (%s) that is not committable from the dashboard — out-of-band approval is tracked in #338. Apply it from the host with `%s apply`.' "${hit:-unparseable change row}" "$0"
printf 'this change alters a security-sensitive setting (%s) that is not committable from the dashboard. Edit config.json on the host and run `%s apply`.' "${hit:-unparseable change row}" "$0"
return 1
fi
if printf '%s\n' "$porcelain" | grep -qE $'^DEST\t'; then
printf 'this change is destructive and cannot be committed from the dashboard — out-of-band approval is tracked in #338. Apply it from the host with `%s apply`.' "$0"
printf 'this change is destructive and cannot be committed from the dashboard. Edit config.json on the host and run `%s apply`.' "$0"
return 1
fi
# Approved: echo the changed key NAMES so the commit's audit entry can record WHAT changed
Expand Down
30 changes: 29 additions & 1 deletion tests/stack/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,34 @@ assert_eq "scan height: auto -> genesis 0 (full payout history)" "$(rsh auto)" "
assert_eq "scan height: empty -> genesis 0" "$(rsh '')" "0"
assert_eq "scan height: explicit block kept verbatim" "$(rsh 2500000)" "2500000"

# Wallet healthcheck (#718): during the multi-hour genesis scan monero-wallet-rpc refuses the RPC,
# so the check must tolerate an unreachable RPC WHILE the initial-scan marker is present, and turn
# strict once the RPC first answers. Stub `curl` on PATH to be the RPC up/down control.
HCBIN="$SANDBOX/hc-bin"
HCDIR="$SANDBOX/hc-wallet"
mkdir -p "$HCBIN" "$HCDIR"
mk_curl() {
printf '#!/bin/sh\nexit %s\n' "$1" >"$HCBIN/curl"
chmod +x "$HCBIN/curl"
}
run_hc() { (
PATH="$HCBIN:$PATH" WALLET_DIR="$HCDIR" sh "$ROOT/build/monero/wallet-healthcheck.sh" >/dev/null 2>&1
echo $?
); }
# RPC down + marker present (mid initial scan) -> healthy (the whole point of #718).
mk_curl 7
: >"$HCDIR/.payout-scanning"
assert_eq "healthcheck: RPC down but scanning -> healthy (#718)" "$(run_hc)" "0"
# RPC up -> healthy AND the marker is retired (scan caught up; strict from now on).
mk_curl 0
assert_eq "healthcheck: RPC up -> healthy (#718)" "$(run_hc)" "0"
if [ -f "$HCDIR/.payout-scanning" ]; then bad "healthcheck: RPC up clears the scan marker (#718)" "marker still present"; else ok "healthcheck: RPC up clears the scan marker (#718)"; fi
# RPC down + NO marker (scan already finished once) -> unhealthy: a real fault, not scan tolerance.
mk_curl 7
assert_eq "healthcheck: RPC down after scan done -> unhealthy (#718)" "$(run_hc)" "1"
# The entrypoint arms the marker on wallet creation so the grace applies from first boot.
assert_contains "wallet-entrypoint touches the scan marker on create (#718)" "$(cat "$ROOT/build/monero/wallet-entrypoint.sh")" 'touch "$SCAN_MARKER"'

echo "== unit: clock_sync_status (mining is time-sensitive) =="
# doctor's NTP check classifies timedatectl's NTPSynchronized: yes→synced, no→unsynced, else unknown.
CLKBIN="$SANDBOX/clk-bin"
Expand Down Expand Up @@ -4814,7 +4842,7 @@ assert_eq "destructive candidate previews destructive:true" "$(jq -r '.destructi
printf '{"id":"%s","action":"commit","actor":"admin"}\n' "$UUID3" >"$REQS/$UUID3.json"
run_pending >/dev/null
assert_eq "destructive commit is refused" "$(jq -r '.status' "$RESULTS/$UUID3.json" 2>/dev/null)" "rejected"
assert_contains "destructive refusal points at #338" "$(jq -r '.error' "$RESULTS/$UUID3.json" 2>/dev/null)" "#338"
assert_contains "destructive refusal names the host apply path, not stale #338 (#713)" "$(jq -r '.error' "$RESULTS/$UUID3.json" 2>/dev/null)" "Edit config.json on the host"
assert_eq "refused destructive commit did not touch config.json" "$(jq -r '.monero.clearnet_initial_sync // false' "$C/config.json")" "false"
[ ! -f "$STAGED/$UUID3.json" ] && ok "refused destructive intent cleared from staged" || bad "refused destructive intent cleared from staged" "still staged"
# A NON-destructive commit still proceeds (pool switch mini -> nano is INFO, not DEST).
Expand Down