release: v1.1.0 — Tor-first privacy#331
Open
VijitSingh97 wants to merge 44 commits into
Open
Conversation
…issing runner (#266) Prep for a long-lived `develop` integration branch (v1.1+ work lands there; main stays at the released state). - ci.yml: run the CI workflow on pushes to `develop` too (PRs already run on any base branch). - release-gate.yml: gate the self-hosted Tier-4 job behind `if: workflow_dispatch || vars.ENABLE_RELEASE_GATE == 'true'`. There is no self-hosted runner registered yet, so every push to main was queuing a job no runner could take, which GitHub auto-cancels after 24h — a permanent red ✗ on main. Now the automatic run is skipped (green/neutral) until a runner is wired up and ENABLE_RELEASE_GATE is set; a manual dispatch still runs. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osed e2e (#144, #252, #203) (#265) * chore(v1.1): Wave 1 cleanup — dead known_workers, tar xattrs, fail-closed e2e (#144, #252, #203) Bundles the three zero-dependency v1.1 Wave-1 cleanup issues into one PR. #144 — Remove the dead known_workers persistence layer (orphaned by the proxy-sourced worker rewrite). Drops the state key, the workers table + its last_seen migration (replaced by a one-line DROP TABLE IF EXISTS to tidy old DBs), the load() block, and update/get_known_workers(). Two deviations from the issue text, both verified against the tree: * The issue named one test to delete; there were actually 4 across two classes (TestWorkers + a retention test) — all removed. * The issue said "keep WORKER_RETENTION_SEC, worker_presence.py uses it", but worker_presence.py doesn't exist here (it's in the unmerged #121 draft, now v1.2). Honored the acceptance criterion (kept the constant for #121) but corrected the false rationale in its comment; it has no live consumer in the current tree. Also simplifies the dead `worker_configs` unpack in data_service.py. #252 — Release bundle carried macOS xattrs (LIBARCHIVE.xattr.*) → GNU tar warns once per file on Linux extract. Adds `tar --no-xattrs` (portable no-op on GNU tar) + a post-bundle guard that greps the stream for xattr pax headers and fails the release if any survive. Validated on real macOS bsdtar: old bundle carries com.apple.provenance, new one is clean. #203 — Live e2e that the DEPLOY path refuses an unauthenticated proxy API with an empty PROXY_AUTH_TOKEN (#153), behind a new opt-in `--auth-fail-closed` phase. Tests `pithead up` (NOT apply — apply self-heals by regenerating the token; up doesn't re-render .env, so the compose `:?` guard fires). Restores the EXACT original token so the harness's end-of-run secret-fingerprint check still holds. Verification: dashboard pytest 528 passed, coverage 94% (gate 80%); shellcheck --severity=warning clean on release.sh + run.sh; integration selftest 97 passed; #252 fix/guard and #203 .env rewrite validated locally. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(wave-1): regen test-inventory + add #144/#252 regression tests Fixes the red CI on this PR: the first commit removed the 4 dead known_workers tests but left docs/test-inventory.md at the old count, tripping the `make test-inventory-check` drift guard (616 → 612). Also adds two regression tests for the Wave-1 changes: * #144 — pytest asserting the orphaned `workers` table is DROP'd in place when an older DB is opened, and the dead known_workers state key is gone, with history/shares/kv left intact. * #252 — a tests/stack/run.sh section asserting release.sh keeps `tar --no-xattrs` + the xattr-pax-header guard, and that this platform's tar actually honours --no-xattrs (reproduces the bug on macOS, no-op on GNU tar). Inventory regenerated to match (drift guard green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(e2e): add e2e.sh gouda branch-runner (lean by default) + docs One-command Tier-4 wrapper that deploys a branch to the live gouda test bench, borrows a real miner, validates, and restores everything (miner pool config + canonical baseline stack) via an EXIT trap — even on failure/Ctrl-C. Design: * Dedicated /srv/code/pithead-e2e checkout; /srv/code/pithead stays the untouched baseline. Compose project is pinned to "pithead", so the two checkouts drive the SAME containers + the SAME shared synced chains — a code/image swap, never a re-sync. * Seeds the e2e checkout with the canonical config.json/.env (same wallet/ secrets/onions/data-dirs). * Borrows miner-0 (config-watch auto-reloads the repoint; restored from a timestamped backup), runs run.sh DETACHED on the box (survives SSH drops). Modes — default is LEAN per the "validate sync logic + dashboard against the node we already have, don't re-sync" guidance: * targeted (default) — dashboard check + sync gate via ONE controlled restart (lifecycle/failover) + --auth-fail-closed. No config sweep. * check — pure reads. * matrix — opt-in full destructive config sweep (pre-release gate). Also waits for monero/tari to re-confirm synced after a deploy (post-restart "loading" → "done") before the harness pre-check, so it doesn't flap — and that wait doubles as a direct sync-detection check. Validated live on gouda: check mode 36/36 live assertions, clean restore; the config sweep passed main/mini/nano sync+dashboard + node-down/unhealthy failover, all reusing the synced chains (no re-sync). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…) (#267) p2pool's --onion-address only ADVERTISES an onion for inbound peers; without a SOCKS proxy it dials outbound sidechain peers over clearnet, exposing the home IP (upstream docs/TOR.MD) — contradicting the stack's privacy default. Route those dials through the bundled Tor proxy by default. - New `p2pool.clearnet` config knob (default false ⇒ Tor). When false, pithead appends `--socks5 ${NETWORK_PREFIX}.25:9050 --socks5-proxy-type tor` to P2POOL_FLAGS via a small pure helper `p2pool_outbound_flags` (subnet-aware, honours a custom network.subnet / #180). `true` = direct clearnet for max yield (lower stale/uncle rate, larger peer set — worse on --mini/--nano). - Fix a latent compose bug surfaced by this change: `- ${P2POOL_FLAGS}` is a SINGLE command item, so Compose passes a multi-flag value as one mangled arg (verified via `docker compose config`). Move P2POOL_FLAGS to the p2pool container environment and word-split it in the entrypoint (`exec p2pool "$@" ${P2POOL_FLAGS:-}`) — also drops the stray empty arg for the main-pool case. Tests (tier 1, all green; full live regression on develop after Wave 2): - unit: p2pool_outbound_flags (Tor default / clearnet opt-out / custom prefix) - entrypoint word-splits P2POOL_FLAGS into distinct args (+ empty → no stray arg) - black-box: a full `pithead apply` renders Tor SOCKS flags by default - compose: P2POOL_FLAGS reaches the p2pool env intact (test_compose green) Docs: privacy.md egress table + hardening section, configuration.md reference. The Tor default is subject to the #256 benchmark (orphan/uncle-rate impact) confirming it before the v1.1 release; the opt-out is the release valve. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) While donating, algo_service points xmrig-proxy at na.xmrvsbeast.com:4247 — which would otherwise expose the home IP to XvB. Route that connection through Tor by default, mirroring the #163 stats-fetch fix for the mining path. - algo_service.switch_miners now sets a per-pool "socks5" on the ENABLED XvB pool object (xmrig-proxy resolves that pool's DNS proxy-side, like socks5h). The local p2pool pool NEVER gets socks5 — it dials direct on the bridge. - New xvb.tor knob (default true ⇒ Tor). false dials the XvB pool direct for max yield (stratum-over-Tor latency raises rejects, scales with hashrate). - XVB_TOR_SOCKS5 is derived from the existing XVB_TOR_PROXY (scheme stripped → bare host:port for xmrig), so a custom subnet (#180) carries through. - xmrig-proxy dev-fee --donate-level is already pinned 0 by default (#173); documented that >0 would bypass the XvB socks5 → clearnet. Tests: test_algo_service — XvB pool carries socks5 by default while the local pool doesn't (both XVB and P2POOL modes), and xvb.tor:false → no socks5 anywhere. Dashboard pytest 94% coverage; pithead suite + test_compose green; shellcheck clean. Docs: privacy.md (now "Tor-by-default for all runtime egress"), configuration.md. Default subject to the #256 benchmark before the v1.1 release; closes #160's flips. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
#270) (#275) * feat(privacy): enforce Tor-only egress fail-closed via a host firewall (#270) Per-app Tor config is fragile — p2pool leaked on a stale image, and Tari dials some peers over clearnet despite type=tor (#271). Make "behind Tor" structural: a misconfigured/buggy bridge daemon now CANNOT reach clearnet. pithead installs rules in Docker's DOCKER-USER chain at up/apply (removed at down): the mining bridge (monerod/p2pool/tari/xmrig-proxy) may reach the LAN, the other containers, and the Tor SOCKS, but any DIRECT clearnet dial is DROPPED — only the tor container reaches the internet. Fail-closed. - Keeps the topology + published ports intact (the internal-network approach was ruled out on gouda: it blocks published ports too). Rules are source/dest-based; ESTABLISHED/RELATED accepted so published-port replies + Tor return traffic work; the Tor container (.25) is exempt. - `network.tor_egress_firewall` knob (default true) → rendered to .env so the `up` path can read it; opt out to fall back to per-app config. - Subnet-aware (#180); needs root (sudo) like the GRUB/HugePages steps; IPv4 (mining_net is IPv4); degrades gracefully if iptables/root unavailable (warns, doesn't fail the stack). Tests (tier 1): tor_egress_rules ruleset (accepts first, DROP last, custom subnet); black-box apply/remove via stubbed iptables (installs tagged rules; opt-out installs nothing). 377 pithead-suite tests green; shellcheck clean. Docs: privacy.md (the structural guarantee) + configuration.md. LIVE VALIDATION (needs root on the box): `pithead up` installs the rules, then `bench-verify-egress.sh tor` confirms 0 app public connections even with a deliberately-misconfigured app. Contains #271 structurally. Part of #160. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(pithead): remove_tor_egress_firewall must not trip `set -e` on a no-match grep (#270) CI (Linux, where iptables exists) failed 3 existing apply/up black-box tests with rc 1. Root cause: pithead runs `set -Eeuo pipefail`, and remove_tor_egress_firewall's cleanup pipeline (`iptables-save | grep | grep | while`) returns non-zero when no tagged rules exist (grep finds nothing) → errexit aborts the whole `pithead apply`/`up`. It passed on macOS only because `command -v iptables` fails there and the function returns early. Capture the matching rules with `|| true` and iterate via a here-string, so a no-match is a clean 0. The firewall removal stays best-effort + idempotent. 377 pithead-suite tests pass both with iptables present (Linux-simulated) and absent (macOS); shellcheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…lose startup window (#276) (#277) * fix(#270): install Tor-egress firewall before containers start (close startup window) The firewall was installed *after* `docker compose up`, leaving a brief startup window in which a clearnet-only app (Tari, #271) opens connections that the ESTABLISHED,RELATED rule then grandfathers past the DROP — observed live as Tari holding 2-3 public peers after a fresh `up`. Move the install ahead of compose so no container ever dials before the rules are in place, and pre-create DOCKER-USER (idempotent) so the pre-compose install also works on a first-ever `up`. DOCKER-USER is a static chain Docker preserves across network (re)creation, so rules referencing the fixed subnet/Tor IP can be installed before the network exists; Docker adds the FORWARD jump when it creates the network. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(#276): assert apply pre-creates the DOCKER-USER chain Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…inely behind Tor (#271) (#285) * fix(#271): route Tari clearnet peer dials through Tor SOCKS (proxy_bypass_for_outbound_tcp=false) minotari's tor transport defaults proxy_bypass_for_outbound_tcp=true, so SocksTransport::dial() direct-dials any peer that advertises a bare /ip4|/ip6 .../tcp address (the seed list ships clearnet variants) — bypassing Tor and leaking the home IP, observed live as sustained connections to public IPs despite type="tor". Setting it false routes every non-onion dial through the SOCKS proxy, so Tari reaches those peers via Tor exit nodes: fully functional AND never touching clearnet directly. Keep proxy_bypass_addresses empty (no per-address exemptions; we run no local Tari wallet). Verified against tari v5.3.0 source (transport.rs:160-212, socks.rs:119-128) and the tari-launchpad docker_rig config. Residual (tracked separately): Tari Pulse does a checkpoint DNS lookup with no upstream off-switch; the node tolerates it being blocked. Merge-mining is unaffected (base node ↔ MM proxy over local gRPC). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(#271): assert tari config routes outbound TCP via Tor SOCKS (proxy_bypass_for_outbound_tcp=false) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…h's images (#287) `e2e.sh deploy_branch` used `pithead apply -y`, which runs `compose up --pull` (never `--build`) — so it tested whatever first-party images were last built on the box, not the branch under test. That masked #165's p2pool clearnet leak (the entrypoint word-split only lives in a rebuilt image). Switch deploy to `pithead upgrade`, which re-renders the generated configs (inject_service_configs) AND rebuilds the first-party images from build/ (--build) before recreating, so a Dockerfile/entrypoint/template change is actually under test. Also log the built image IDs so "what we tested" is unambiguous. Plus: `stack_upgrade` now reasserts the Tor-egress firewall (#270), consistent with up/apply — previously the upgrade path left it to persist from a prior up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(#274): standing no-clearnet-leak egress gate in the harness Promote the #256 egress verifier into the live harness as a standing privacy assertion (the runtime proof of #270): the --check phase now FAILs if any app container holds a persistent direct public connection — what config-level checks miss (it caught the #165 stale-image p2pool leak and the #271 Tari direct-dial). Refine bench-verify-egress.sh: poll N times (default 4×10s) and flag only IPs seen in >= --min-hits polls, so post-restart startup transients (a brief direct dial before Tor circuits build) don't false-positive — only sustained leaks fail. Validated on gouda: caught a grandfathered Tari leak (2 persistent IPs → FAIL), then a clean PASS (all 4 apps via Tor) after a restart cleared it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#274): regenerate test inventory for the egress-gate assertion Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ompose↔image coupling) (#289) #165 moved P2POOL_FLAGS to the container env, word-split by a new entrypoint. A 'new compose + old image' partial update leaves the old entrypoint ignoring the env → p2pool launches with no --socks5 and dials the sidechain over CLEARNET, silently. #270 now DROPs that dial (so a stale p2pool can't peer rather than leaking) and #272 rebuilds on deploy — but make the CAUSE obvious instead of a mysterious can't-peer stall: - pithead doctor: when clearnet is off (P2POOL_FLAGS carries --socks5), read the RUNNING p2pool argv from /proc/1/cmdline (the env flags are word-split in the entrypoint, so they never show in the container Args) and FAIL with a rebuild hint if --socks5 is absent. Image-independent (pithead is always current). - p2pool entrypoint: log the final launch command, so the applied flags are auditable in `docker logs p2pool`. Tests: doctor FAILs on a stale (no-socks5) p2pool argv, OK when --socks5 present. Live: confirmed gouda's p2pool exposes --socks5 in /proc/1/cmdline. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ridge) (#290) p2pool's --socks5 (#165 Tor sidechain routing) ALSO proxies its monerod RPC/ZMQ connection — and p2pool exempts ONLY loopback (127.0.0.1/::1/localhost) from the proxy (p2pool v4.16 json_rpc_request.cpp / util.cpp is_localhost). So with the default p2pool.clearnet=false, p2pool dialled the local monerod's private Docker IP THROUGH Tor → "get_info ... empty response" → no template → no mining. There's no per-host proxy-bypass flag. Fix: when --socks5 is on, the entrypoint bridges 127.0.0.1 -> the real node with socat and repoints --host at loopback, so the node RPC/ZMQ stay DIRECT while the sidechain P2P still rides --socks5 over Tor. Reads the node/ports from the args (no compose coupling); no-op when clearnet (no --socks5) or node already loopback. This was masked until #272 (e2e now rebuilds) — prior e2e ran the pre-#165 p2pool with no --socks5. Without it, the v1.1 Tor default produces zero shares. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g, pre-commit) (#296) * style(#280): apply ruff format to the dashboard (format-only) Wave 7 tooling. Adds the [tool.ruff] format config (target py311, line-length 100, generated Tari gRPC stubs excluded) plus a repo-root .editorconfig, then runs `ruff format` across build/dashboard. Pure formatting, no behaviour change: the dashboard pytest suite (524 tests) is green before and after. The lint gate lands in a separate commit so this large, zero-logic diff stays reviewable (per #280). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(#280): enable the ruff lint gate for the dashboard Wave 7 tooling. Turns on `ruff check` (E/F/W/I/B/UP/ASYNC/S) alongside the formatter from the previous commit, wired into `make lint-py` (folded into `make lint`) and a new `python-lint` CI job. ruff is pinned via a new `dev` extra so local, pre-commit, and CI all run the same version. Findings resolved: - auto-fixes: import sorting (I), unused imports (F401), pyupgrade to PEP 585/604 (UP) — safe under the py311 target + python:3.11 runtime. - real fixes: chain the re-raised HTTPException (B904); explicit zip(strict=False) (B905); drop a dead tari_status_str/tari_active block (F841 — likely latent UI-status bug, flagged for follow-up). - documented `# noqa` for verified-safe security hits: demo creds in a __main__ example + CSS palette tokens (S105), constant-built SQL with ?-bound values (S608), probabilistic-pruning random (S311), best-effort disk stat (S110). Sentinel "0.0.0.0" strings ignored globally (S104); E501 is owned by the formatter; tests ignore S101/S106/B017/ASYNC240. Dashboard suite green on 3.11: 532 passed, 94% coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(#280): add pre-commit config + document the dev tooling Wave 7 tooling. Adds .pre-commit-config.yaml — ruff-check + ruff-format pinned to v0.15.17 (matching the dev extra), plus detect-private-key / check-added-large-files / end-of-file-fixer / trailing-whitespace, with generated gRPC stubs and vendored/minified assets excluded — and documents the one-time `pip install -e "build/dashboard[dev]" && pre-commit install` setup in CONTRIBUTING.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * tooling(#280): lint all repo Python (root ruff config + integration fakes) Extends ruff coverage beyond the dashboard package to the rest of the repo's Python — the tests/integration/fakes/* contract-test helpers were unlinted. Adds a root ruff.toml that `extend`s the dashboard's canonical rules, so `ruff`/pre-commit discover a config from anywhere (dashboard files -> package pyproject; repo-level files -> root config) and Make/CI/pre-commit all lint the same set. `make lint-py` now runs from the repo root; pre-commit's ruff hooks drop their build/dashboard scoping. Fakes: import-sort + format only (no logic change); their contract test still passes (12). Also gitignores .venv/.ruff_cache. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* build(#283): reproducible Python builds with uv + uv.lock Wave 7 tooling. Adopts uv and commits a hashed uv.lock so the dashboard's dependencies resolve identically across Docker, CI, release, and local — closing the last unpinned supply-chain surface (loose `>=` floors) after the digest-pinned bases (#135) and SHA256-verified binaries. - uv.lock: 42 packages, hash-pinned, generated from pyproject (still the dep source of truth). Dependabot will bump it (#282). - Dockerfile (both stages): uv binary COPYed from the digest-pinned ghcr.io/astral-sh/uv image; `uv sync --locked` (prod) / `--extra test` (test) into /app/.venv on PATH, so entrypoint.sh's `python3` is unchanged. Test/dev extras are excluded from the prod image. - CI, Makefile, and the release path install/run from the lock via `uv run --locked`; ruff lint runs via uv from the locked `dev` extra. - uv installed in CI from the version-pinned installer URL (same posture as the curl-pinned hadolint), not a mutable-tag action. - Last pip reference (Tari stub regen) moved to uvx. - CONTRIBUTING / README / pre-commit docs updated for the uv workflow. Verified locally: Docker test stage runs 532 tests + 94% coverage in-container; prod image imports the app + runtime deps from /app/.venv with no pytest/ruff present; `make test` green (dashboard 532, selftest 97, fakes 12, lint sh+py). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(#283): uv best-practice polish (cache mounts, .python-version, drop redundant PYTHONPATH) Follow-up tidy from a best-practices review of the uv adoption: - Dockerfile: swap UV_NO_CACHE for BuildKit cache mounts on the `uv sync` steps (the documented uv-in-Docker pattern) — faster rebuilds, and the cache stays out of the image layers. - Add build/dashboard/.python-version (3.11) so local `uv` defaults to the same interpreter as the digest-pinned Docker base and CI. - Drop the now-redundant PYTHONPATH=build/dashboard from the fakes test (Makefile + CI): uv editable-installs the project into the venv, so the source no longer needs to be on sys.path. Verified the fakes still pass (12). Re-verified: docker test stage (532 tests in-container) + prod stage build with cache mounts; make test-dashboard/test-fakes/lint-py green; uv lock --check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dabot, SHA-pinned actions, zizmor) (#298) Wave 7 supply-chain hardening: SHA-pinned actions, zizmor (+ least-privilege permissions & persist-credentials), gitleaks (CI + pre-commit), Trivy image scanning (--ignore-unfixed + .trivyignore), Dependabot (actions/uv/docker), and uv dropped from the production image. Closes #282.
…t, markdownlint, buf, taplo) (#302) Wave 7: every file surface under a linter/formatter — shfmt, Biome, yamllint, markdownlint+lychee(scheduled), buf, taplo — glued by the Makefile, pre-commit, and a new lint-surfaces CI job. Includes the docker-compose dashboard re-indent (yamllint indentation rule re-enabled). Closes #281.
…ode/upload-artifact) (#299) Dependabot: SHA-pinned action bumps to current majors (checkout v6.0.3 — same as RigForge, setup-python v6.2.0, setup-node v6.4.0, upload-artifact). CI green.
…n protobuf/grpcio) (#303) docker ecosystem ignores semver-major/minor (digest+patch only); uv ignores protobuf/grpcio majors (stub-constrained). Prevents the breaking base/protobuf bumps Dependabot first proposed.
Dependabot: digest-only security updates within the pinned tags (config now ignores major/minor). Trivy + image builds green.
Dependabot: safe minor/patch dep bumps to uv.lock (protobuf/grpcio majors now ignored by config). CI green.
…methodology (#293) * docs(#256): finalize the Tor-vs-clearnet benchmark plan + bring forward the collector Finalize the methodology against real numbers: full fleet miner-0..7 (~269 kH/s) on the `mini` sidechain (~1.8% of it → ~155 of our shares/day — representative + enough power; `nano` would make us ~11% and overstate the penalty, `main` is too sparse). Switch the design to an INTERLEAVED A/B (T/C/T/C in ~1.5-2d blocks, discard ~6h post switch) to control time-correlated variance, run to a target share count, and use the clearnet arm's own variance as the noise floor. Add the "autonomous on gouda" section + runbook: the run lives entirely on the box (detached orchestrator + cron watchdog + @reboot) so it survives the operator being offline for days; observe over Tailscale via `bench-status`. Scripts (orchestrate / healthcheck / status) land next, validated on gouda before the real run. Brings bench-collect.sh onto develop (was only on the #256 draft); supersedes #268. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#256): autonomous benchmark harness (orchestrator + healthcheck + status) Three gouda-resident scripts so the Tor-vs-clearnet run survives the operator being offline for days: - bench-orchestrate.sh — interleaves the arms on a fixed block schedule (T/C/T/C), egress-gates each switch (rigorous multi-poll), keeps the collector running, health-checks each cycle, writes a status.json heartbeat, and self-heals (re-up stack + re-gate on BROKEN). All state in ~/pithead-bench, so a crash/reboot resumes from state. Subcommands: run | --calibrate | --watchdog | --status | --stop | --install-cron (the watchdog cron + @reboot restart it if it dies). - bench-healthcheck.sh — read-only checklist -> OK/WARN/BROKEN (stack, p2pool, hashrate, Tor-arm egress + firewall); reused by the loop + the cron. - bench-status.sh — one-screen summary for `ssh gouda bench-status` over Tailscale. shellcheck clean. To be validated on gouda (dry-run) before the real fleet run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): float-safe block/settle hours in the orchestrator (hours→secs via awk) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): healthcheck uses hashrate_15m (not 1h) so it doesn't false-WARN during the ramp Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): gate self-heals grandfathered Tari leak (restart) + bench-status missing-jsonl guard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): per-cycle healthcheck egress matches gate rigor (3-poll/30s) to avoid false BROKEN on brief Tari dials Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): healthcheck keys on proxy_workers (reliable), not p2pool's noisy share-based hashrate estimate Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): bench-status leads with 'stable for Xh' so quiet runs don't read as failing (old launch events linger in the tail) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): disable XvB for the benchmark — 'auto' optimizer was routing the fleet to XvB (target Whale), starving p2pool and confounding reward_share The XVB_DONATION_LEVEL=auto optimizer dynamically splits the fleet to climb raffle tiers — observed ~96kH/s to XvB vs ~18kH/s to p2pool while chasing the Whale tier. That starves p2pool (so reward_share, the primary metric, is measured on a fraction of the fleet) and makes the split a function of Tor latency — the exact thing we isolate. Disabling XvB sends the full ~269kH/s to p2pool in both arms. Re-asserted on every apply so a switch/recovery can't let it drift back on; --stop restores xvb.enabled=true. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#256): clearnet arm must toggle the #270 egress firewall OFF, else p2pool gets 0 peers set_arm flipped p2pool.clearnet but left the Tor-egress firewall (#270) ON, which DROPs direct clearnet dials from the container subnet. The clearnet arm would have collected zero-peer/zero-share garbage for 3 of 6 blocks — and since both runs so far only exercised block-1 TOR, it would have first bitten at the first switch (~20h in), unattended. Now the firewall tracks the arm: ON for tor (fail-closed, egress-gated), OFF for clearnet (p2pool peers over clearnet — the baseline). --stop restores firewall=true so a mid-clearnet-block stop never leaves gouda open. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(#270): network.tor_egress_firewall=false never disabled the firewall (jq // false-coercion) `jq -r '.network.tor_egress_firewall // true'` returns true even when the key is explicitly false, because jq's // alternative treats false (not just null) as empty. So the documented #270 opt-out has never worked — the firewall was always on regardless of config. Same latent bug on `.xvb.tor // true` (xvb.tor=false silently stayed on Tor). Both now null-check explicitly so a configured false is honoured; absent still defaults on (fail-closed / Tor-by-default preserved). Surfaced by the #256 benchmark: the clearnet arm sets tor_egress_firewall=false so p2pool can peer over clearnet, but the firewall stayed up → 0 sidechain peers. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(#294): extract config_bool helper + regression test for the jq false-coercion bug Replaces the two inline null-checks (TOR_EGRESS_FIREWALL, xvb.tor) with a shared config_bool() helper that honours an explicit false, and adds tests/stack unit coverage (explicit false/true, absent→default). The prior firewall test only exercised apply_tor_egress_firewall reading .env — it bypassed the config.json→.env jq that actually had the bug. Behavior-identical to d4b5df3 (which is what gouda's live benchmark runs); this is the cleaner, tested form for develop. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(#256): formalize Tor-vs-clearnet results — Tor costs ~10% p2pool yield (latency, not rejects) The 5-day interleaved mini run completed (6 blocks, 3/arm). Result: Tor is -11.4% reward-share / -11.5% yield-efficiency vs clearnet, just above the ±8-10% block-to- block noise floor — so 'roughly 10%', directionally robust, not a tight estimate. Mechanism is pure propagation latency: 0 rejects in both arms, equal Tor-daemon CPU (~19%), equal peer counts; effort higher on Tor (113% vs 90%) = uncles/late shares. Recommendation: keep Tor the default (#165/#166) — ~10% is a modest, opt-out-able price for not leaking the home IP. Adds: - bench-analyze.py: committable analyzer (block = stat unit, settle excluded) - the raw run data under docs/benchmarks/data/ (reproduces the table exactly) - Results section in the methodology doc + the measured cost in privacy.md (#165/#166) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(#256): finalize Tor-vs-clearnet write-up + align the Tor-by-default story across docs - tor-vs-clearnet.md: flip the banner from 'results pending' to complete; reconcile the Method/runbook params with what was actually run (20h blocks / 3h settle / 6 blocks / ~5 days, not the planned 1.5-2d / 6h / 10-12d), and drop the non-existent --target-shares flag. Frame methodology + decision rule as pre-registration. - faq.md + architecture.md: #165/#166 were still described as 'planned for v1.1' / 'slated to move' and listed under 'what still touches clearnet' — now Tor-by-default as of v1.1 with the measured ~10% opt-out cost, consistent with privacy.md/configuration.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(#256): satisfy develop's ruff+shfmt+markdown lint on the benchmark files After merging develop (which added the Wave 7 lint tooling), CI applied ruff/shfmt/ markdownlint to the new benchmark files: - bench-analyze.py: drop unused median import, lambda->def, and use calendar.timegm instead of datetime.UTC so the documented 'python3 bench-analyze.py' reproduce command stays portable to pre-3.11 Python; ruff-format applied - bench-{collect,healthcheck,orchestrate,status}.sh + tests/stack/run.sh: shfmt -i 4 - tor-vs-clearnet.md: blank line around the Findings list (MD032) No behavior change. lint-sh/lint-py/lint-md + the pithead suite all pass; the analyzer reproduces the committed run's table byte-for-byte. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(#256): regenerate test-inventory for the config_bool regression test develop's CI gained a 'make test-inventory-check' gate; the new tests/stack config_bool case (#294 jq false-coercion guard) needs to be enumerated. No content change beyond the regenerated inventory. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ternal, Tor vs clearnet) (#308) * feat(#170): Component Health panel — per-component egress posture (Tor vs clearnet) Adds a dashboard panel + header roll-up badge showing, for every stack component, its outbound connections tagged with the network route the server derives from live config (Tor / clearnet / local / inactive) — the runtime, at-a-glance counterpart to the #160 privacy audit. - service/egress.py: derives each component's connection routes from config (p2pool.clearnet, the #270 firewall, xvb.tor/enabled, monero/tari clearnet-sync, remote-node), never hardcoded. Models the two backstops honestly: the firewall (fail-closed) neutralises a *container's* clearnet route, but NOT the host-networked dashboard's own egress — so disabling XvB-over-Tor is flagged as a real leak even with the firewall on. Summary roll-up: "All egress via Tor" vs "N clearnet paths". - /api/state gains an "egress" key + a header badge (🛡️ Tor-only /⚠️ N clearnet). - Frontend: ComponentHealth panel (components.mjs) + egressRoute() map (logic.mjs) + CSS. - Plumbing: pithead renders P2POOL_CLEARNET; compose passes it + TOR_EGRESS_FIREWALL to the dashboard. - Tests: test_egress.py (8, incl. the host-net-leak nuance) + egressRoute frontend test. Subsumes #295 (Tari status is now surfaced in the panel). Verifiable on an idle stack with no miners (egress posture is live regardless). Closes #170. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(#170): topology view — full stack wiring (ingress/egress/internal, Tor vs clearnet) Reworks the Component Health panel from an egress-only list into a trust-boundary topology map. The audit of the live stack found the list was missing all ingress (rigs→xmrig-proxy :3333, browser→caddy :443, onion inbound to the daemons), the tor SOCKS/onion hub itself, and the internal mesh (p2pool→tari merge-mine, the dashboard's polling fan-out). Backend: service/egress.py grows compute_topology/topology_from_config — nodes + edges (ingress/egress/p2p/internal, route tor/clearnet/local/inactive). A clearnet route lands on the `internet` node so a leak visibly bypasses the Tor hub. The summary is shared verbatim with the egress list so the badge can't disagree. Frontend: new topology.mjs (StackTopology) renders a data-driven SVG — green Tor / red clearnet edges, double-arrows for P2P (egress + onion ingress), an internal-mesh toggle, hover detail. boxAnchor() (logic.mjs, unit-tested) routes connectors to box borders. The per-component egress list is demoted to an expandable drawer. Tests: +10 topology backend, +1 boxAnchor frontend. /api/state gains `topology`. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…278) (#314) p2pool's --socks5 (Tor) proxies every non-loopback outbound. #278 moved the monerod RPC/ZMQ legs onto a socat loopback bridge so they stay direct, but the Tari merge-mine gRPC leg was missed: --merge-mine tari://<private-docker-ip> was dialled through Tor, which rejects RFC1918 ("Rejecting SOCKS request ... to private address"), leaving channel_state stuck at TRANSIENT_FAILURE — no Tari merge-mined while Monero mined fine. entrypoint.sh: when --socks5 is set and the --merge-mine host is non-loopback, socat-bridge 127.0.0.1:PORT -> the real node and rewrite the URL host to the 127.0.0.1 IP literal (p2pool's TCPServer::connect_to_peer only exempts loopback IP literals from the proxy; "localhost" parses as a DomainName and would still be proxied). Mirrors the monerod bridge exactly. dashboard: the "✔" was gated on `active` (a chain is merely configured), so a dead channel rendered as "TRANSIENT_FAILURE ✔". Add `connected` (channel_state == READY) and gate the ✔/green on it; show a warn state with no ✔ otherwise. Tests: stack entrypoint test (rewrite + bridge + clearnet no-op), pools/views unit tests for connected. 392 stack + 563 dashboard pass; ruff + shellcheck clean. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* test(#170): end-to-end coverage for Stack Topology & Egress The #170 panel's data derivation was well unit-tested, but two layers had no guard: the backend↔frontend node contract (the SVG silently drops any edge whose endpoint it can't place) and the /api/state wiring + header badge. Close both, sweep every config combination, and fill a couple of adjacent frontend gaps found along the way. Backend (tests/service/test_egress.py, +7): - Exhaustive 2**7 sweep over all config knobs asserting every edge endpoint is a placeable node, every edge is well-formed, and the topology summary is byte-identical to the egress summary for every combination. - Pin TOPOLOGY_NODES to a canonical id set (the contract the frontend mirrors). - Tari clearnet-sync path (was only covered for monerod). - Multi-leak counting: firewall-off counts every clearnet path; firewall-on blocks containers but not the host-networked dashboard. Frontend (tests/frontend/topology.test.mjs, new): - POS covers exactly the canonical node set — the other half of the contract, so a backend/frontend drift fails CI instead of vanishing from the diagram. - edgePath yields a valid, finite SVG path for every node pair; the column-crossing lanes route orthogonally; the route palette covers all four routes. Exports POS/edgePath/ROUTE_* from topology.mjs to test them. View layer (tests/web/test_views.py, +5): - build_state surfaces egress + topology with a shared summary; safe config emits a Tor-only badge; a host-dashboard clearnet leak flips it to a warning; remote monerod is reflected end to end. Adjacent gap (tests/frontend/chart.test.mjs, new): - chart.mjs withAlpha + padYAxis were untested (fill-colour rule + near-flat y-padding maths). Exported and covered. Regenerated docs/test-inventory.md (+12 backend, +11 frontend tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#170): render-layer smoke tests for the dashboard components components.mjs (every Preact card, including the #170 ComponentHealth panel) was the only static module with no coverage — exercised solely by the browser smoke test. Close that without breaking the repo's no-package.json / no-DOM policy. Approach (dependency-free, no toolchain): - tests/frontend/helpers/render.mjs — a ~40-line Preact vnode→string walker. The components use no hooks and confine browser work (Chart.js, refs) to componentDidMount, so a static walk reproduces first-mount output with zero deps. Handles functional + class components, Fragment, host elements, arrays. - tests/frontend/fixtures/state.json — a real build_state() payload (the true /api/state contract), generated by fixtures/_gen_state.py so the tests run against the server's actual shape, not a hand-built guess. components.test.mjs (13 tests) drives every card THROUGH the exported App root (components.mjs exports only App by design — no new exports needed), varying the fixture to reach each branch: - App shell: loading (connected vs not), theme switcher, disconnected banner. - Header: brand, server/version/update badges, High Usage only when hot. - Operational: hero band + headline cards render. - WorkersTable: headers, a row per worker with status classes + pool badge, empty-workers, ProxyTotals hidden-until-data. - ComponentHealth (#170): Tor-only summary + topology nodes + egress drawer; flips to a⚠️ warning on a leak; omits the drawer when egress is absent. - Sync mode: gauges instead of the dashboard. Regenerated docs/test-inventory.md (frontend node tests: 46 -> 59 runtime). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#170): e2e contract for topology/egress + round out component cards Two follow-ups from the e2e-coverage audit. 1. Close the e2e gap for the #170 panel. The mini-stack docker e2e (CI, real dashboard vs fake daemons) pinged /api/state but never inspected the body, and the gated config-matrix asserted sync/pool/workers but not the panel — so nothing proved topology/egress flow through the live server. - mini-stack/run-mini-stack.sh: a "scenario 0" that fetches /api/state from the running dashboard and asserts the full #170 contract — egress+topology present, summary shared verbatim, canonical node set, every edge endpoint placeable, egress header badge present, db_healthy bool. - integration/run.sh: matrix assertions (egress/topology present, the shared-summary invariant, canonical node set) alongside the existing runtime /proc/net/tcp egress check. Config-independent, so they hold for every scenario. shellcheck + shfmt clean. 2. Round out the component render tests: the remaining advanced cards (GlobalStats, NetworkCard, TariCard) and EarningsCard's fallback-vs- calculator branches. Frontend node tests: 59 -> 61. Inventory regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…315) The dashboard probed each worker's xmrig /1/summary up to three ways per poll (no-auth, proxy-token, name-token) until one returned 200. That was the source of the per-miner "GET /1/summary 401" log spam, ~3x the worker requests, and a silent {} on total failure — so a misconfigured worker API looked identical to an offline miner. Replace it with ONE probe derived from config: - workers.api_auth: none (default) | name | token (+ api_token, api_port) - default `none` = an open, read-only API (the stock RigForge worker) - on failure: api_ok=false + ONE deduped WARNING per worker (url / status / fix hint), surfaced inline as an "api ⚠" badge on that worker's row — distinct from "offline" - SSRF guard (#122) preserved verbatim; enrichment stays best-effort (the worker still shows via proxy data) Plumbed config.json workers.* -> .env -> docker-compose -> dashboard. Auth is opt-in on both sides; pairs with the RigForge change that opens the worker API by default. Closes #171; addresses the API port/auth half of #172 (per-worker host/port/token overrides remain). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…316) * fix(#291): apply Tor-egress firewall BEFORE compose in stack_upgrade #272 added apply_tor_egress_firewall to stack_upgrade AFTER compose_up_checked, reopening the startup window that #276 closed for stack_up. When the firewall isn't already installed as `pithead upgrade` runs — e.g. `pithead down` then `pithead upgrade` — starting the containers first lets a clearnet app (Tari, #271's firewall-blocked residual) open a connection that the leading ESTABLISHED ACCEPT then grandfathers past the DROP. Observed on gouda: Tari came up holding two grandfathered public peers until a `docker compose restart tari`. In normal operation the firewall is installed at `up` and persists across `upgrade` (Docker never flushes DOCKER-USER), so the window only bites on the down→upgrade path — low severity, but a real consistency gap. Move apply_tor_egress_firewall to before compose_up_checked (after the .env render so the toggle/subnet are current), matching stack_up. Add a regression test pinning the firewall-before-compose ordering: it neutralises the upgrade preamble and records the order of the two load-bearing ops, failing against the pre-fix (compose-then-firewall) sequence. Refs #270 #272 #276. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#291): apply the firewall-before-compose fix to apply + reset-dashboard; fix shfmt CI's shfmt rejected the compact one-line stub block in the new regression test — reformat to one statement per line. While making the invariant uniform across every command (the point of #291), audit found two more paths that start a clearnet-capable container WITHOUT the firewall already in place: - apply: same after-compose ordering as #272's stack_upgrade — on a `down` -> `apply` it recreates containers, then installs the firewall, leaving the identical grandfather window. Move apply_tor_egress_firewall before compose_up_checked (after the .env is committed). - reset-dashboard: recreated dashboard + p2pool but NEVER asserted the firewall. On a running stack the rules persist from `up`, but on a `down` stack it brought p2pool (clearnet-capable) up with no firewall at all. Assert it before the `compose up`. Extend the regression test to pin firewall-before-compose for upgrade, apply AND reset-dashboard (a shared fw_then_compose helper extracts the two sentinels from each command's output). All three fail against the pre-fix ordering. Update the firewall header comment to list every path. Refs #270 #272 #276. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: regenerate test-inventory for the new #291 ordering section `make test-inventory-check` (CI drift guard) flagged the count: the new regression test adds one `pithead` shell section (48 -> 49). Regenerated docs/test-inventory.md via `make test-inventory` — it's a generated file. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#291): close the firewall-coverage gaps — stack_up ordering + the removal path The regression section claimed to cover "every command" but skipped stack_up itself (the reference path #276 fixed) and the black-box section named "apply/remove" only ever exercised apply. Two additions: - Pin firewall-before-compose for stack_up too, so a future reorder of the reference path is caught alongside upgrade/apply/reset-dashboard. - Test remove_tor_egress_firewall against a realistic iptables-save replay: it must -D only our tagged rules and spare foreign DOCKER-USER rules. This removal is the PRECONDITION for the whole #291 window (down strips the firewall, then upgrade/apply/reset bring containers up before reinstalling) — it was previously untested (the apply test's iptables-save stub returned empty, so removal never had anything to delete). Both verified to fail against a broken implementation: a reordered stack_up yields compose-then-firewall, and a wrong tag emits zero -D commands. No new section headers, so the test inventory count is unchanged. 399 passed. Refs #270 #276. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…p) (#317) * feat(#263): auto-register miners with the XvB raffle (no manual signup) The stack mined to the XvB pool and read raffle stats but never registered miners with XMRvsBeast, so XvB-enabled users could donate without actually being entered in the raffle. Add a register() method to XvbClient (mirrors get_stats: full wallet address, Tor SOCKS5, 20s timeout) and wire auto-registration into the data loop, gated on XvB-enabled + a P2Pool PPLNS share existing (the endpoint no-ops before a share lands, so we retry on the next poll). Registration is idempotent and re-runs on a daily cadence so a long-offline rig re-enters cleanly. Persists registered_at to the XvB state and logs success. The XvB operator deliberately does not publish the registration endpoint, so no URL ships in this repo: the submit URL is read from XVB_SUBMIT_URL (empty default => registration is a no-op) and the maintainer injects it at deploy time. The call carries the full wallet and rides the same Tor egress as the stats fetch, so it never leaks the host IP on clearnet (#163). Docs: FAQ "you're auto-registered once you have a share" note + privacy egress table row. Tests for register() and the data-loop gating. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#263): harden XvB auto-registration — edge-case warnings + dashboard status Make the failure/edge modes loud instead of silent (the whole point of #263 is to not *quietly* skip the raffle): - Fallback + warning: XvB enabled but XVB_SUBMIT_URL unset → warn ONCE and skip the wasteful Tor call (instead of a silent per-poll no-op), and surface it. - Warning: a configured-but-refusing endpoint → "failing" status after a few consecutive attempts (transient first-share blips don't alarm; a real outage does). A failed daily re-register after a prior success keeps the ✓. - Dashboard visibility: persist registration_state + registered_at; expose via Metrics; header badges — "XvB raffle ✓" / "⚠ XvB raffle not configured" / "⚠ XvB registration failing" (warnings take priority over a stale ✓). Tests: unconfigured warn-once + status, failure-threshold escalation, success resets the counter, metrics surfacing, and the three badge states (incl. disabled => no badge). Patch coverage 100%. Docs: configuration.md "XvB raffle auto-registration" subsection (env var, badges, fallbacks) + FAQ badge note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#263): bake in real XvB endpoint + classify its real response contract Confirmed the registration API and contract with the XvB operator (DM) and verified it live against the endpoint. Default endpoint (works out of the box): XVB_SUBMIT_URL now defaults to the real endpoint, assembled from the already-public cgi-bin base + a base64'd script name rather than a plaintext, code-search-greppable string — the operator asked us not to *publish* it ("to mitigate abuse"). Override with a URL (e.g. a test server), or a disable sentinel (off/none/false/disabled/0) to turn registration off while keeping XvB on. Real contract (the endpoint is NOT a 200/JSON API — verified live): 2xx => freshly registered 422 "ERROR: Wallet Address Already Registered" => already in (idempotent success) 422 "ERROR: Invalid Wallet Address" => bad wallet (permanent) no-share / 5xx / other => transient, retry register() now classifies status+body into REG_OK / REG_INVALID / REG_NOT_ELIGIBLE / REG_ERROR / REG_DISABLED instead of treating only HTTP 200 as success — the old code would have marked the idempotent "already registered" steady state (every call after the first) as a failure. Fallbacks/edge cases: already-registered = success; invalid wallet latches off + warns once + "⚠ XvB wallet rejected" badge; not-eligible-yet retries quietly; transient errors escalate to "⚠ XvB registration failing" only after N attempts. Tests: full register() contract matrix, data_service status handling (incl. invalid-latch + quiet-retry), config default/override/disable-sentinel, badges. Patch coverage 100%. Also validated register() end-to-end against the live endpoint (already-registered -> registered, invalid -> invalid_wallet). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#263): plaintext XvB endpoint + assert registration is always Tor Per maintainer call: base64 is trivially decodable, so it was obfuscation theater — drop it and commit the real endpoint as plaintext (still kept out of user-facing docs/README per the operator's "don't advertise" ask, but no point hiding it in source). Removes the now-unused base64 import. Tor: registration already routed over XVB_TOR_PROXY (main.py builds XvbClient without a tor_proxy → the bridge Tor default), same path as the stats fetch and NOT subject to the xvb.tor donation opt-out — so the full wallet never hits clearnet. Made that explicit in the config/register() comments and tightened the test to assert register() uses the configured XVB_TOR_PROXY (not just any socks5h). Docs/compose updated to drop the "assembled/not committed" wording. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#263): regenerate test inventory for the new XvB registration tests `make test-inventory-check` (CI drift guard) flagged docs/test-inventory.md as stale after the register()/data_service/config/views/metrics tests were added. Regenerated via `make test-inventory`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…tari derivation (#318) Investigated the dead block #280 removed from data_service ("effective Tari status for UI display" — tari_active/tari_status_str, assigned but never read). Finding: it was genuinely redundant. The effective Tari status IS derived and surfaced — by build_tari() in views.py, which computes the exact same thing (active, the "Waiting..." fallback, and connected-gates-the-✔). The full chain is intact: get_tari_stats() → latest_data["tari"] → build_tari() → state["tari"] → the TariStatus frontend component. No UI bug; #280's deletion was correct. To close this out durably: - Added a comment in build_tari marking it the SINGLE source of effective Tari UI status (so the dead block can't be reintroduced). - Added the two derivation branches TestTari didn't yet cover: active-but-status- missing => "Waiting..." fallback; connected=True while inactive => not connected. No production behaviour change. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ointers (#319) Add THIRD_PARTY_LICENSES.md listing the redistributed components (bundled binaries, vendored JS, Tari gRPC stubs) with their licenses, and — for the two GPLv3 binaries we publish unmodified (p2pool, xmrig-proxy) — the corresponding upstream-source pointers. Scope LICENSE + README so MIT covers our own code and bundled files keep theirs. Record the vendored-JS licenses in their README. GPLv3 is mere aggregation here (unmodified, separate containers, no linking), so the orchestrator stays MIT. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…320) * feat(#255,#91): run all containers as non-root + security follow-ups #255 — drop uid 0 inside every container. Each built image declares a non-root USER (uid 1000): monero/p2pool/xmrig-proxy reuse ubuntu:24.04's 'ubuntu' user, the debian-slim dashboard gets a 'pithead' user; the pulled tari image is pinned via compose `user: 1000:1000`. pithead is the ownership authority — it already chowned the data dirs, now to APP_UID:APP_GID to match the in-image uid. ensure_owner() migrates an existing install's root-owned chain data in one conditional chown -R on the next apply/upgrade (and in the restore path), staying sudo-free in steady state. The dashboard now owns its volume, so it finally drops CAP_DAC_OVERRIDE and joins the other leaf services at cap_drop:[ALL]. p2pool keeps mlock via the unlimited memlock ulimit (cap_add is non-effective for a non-root process; SYS_NICE is best-effort). SECURITY.md + docs/architecture.md updated. #91 — security follow-ups: - dashboard network_mode: host evaluated and consciously kept (it needs the 127.0.0.1 monerod RPC + host-published proxy/SOCKS endpoints; bridge would be a net-negative re-plumb). Rationale recorded in compose. - Tari gRPC allow_methods trimmed to the node-status + merge-mining surface the dashboard (get_tip_info/get_sync_progress) and p2pool actually call; wallet/tx/UTXO/payment-ref/mempool-tx/DAN methods removed. - assert_safe_dir tightened: also refuses bare mount/parent roots and the user homes, plus non-absolute and '..'-traversal paths, before chown -R/rm -rf. NOTE: needs a `docker compose up` boot test on a live host before merge — HugePages/memlock for p2pool, monerod LMDB, dashboard SQLite, and the tari uid override are runtime-validated, not coverable in unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(#255): move monero/p2pool data out of /root to the non-root user's home Running as the 'ubuntu' user (uid 1000) but keeping data under /root was inaccurate and forced a `chmod 0755 /root` hack. Relocate both services to /home/ubuntu — for monerod this is also its native $HOME/.bitmonero default, so the in-container layout now matches a standard install: - monero: data /root/.bitmonero -> /home/ubuntu/.bitmonero (config data-dir + log-file, entrypoint TEMPLATE_PATH/CONFIG_PATH defaults, compose mounts) - p2pool: data /root -> /home/ubuntu (compose mount) - Dockerfiles: drop the chmod /root hack; create+own /home/ubuntu, WORKDIR there - updated the integration tests, docs/configuration.md and the release.sh bundle comment that referenced the old container paths Host data paths (${MONERO_DATA_DIR}/${P2POOL_DATA_DIR}) are unchanged, so no data migration beyond the ownership chown already handled by ensure_owner. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#255): update compose hardening invariant for non-root dashboard The compose security test pinned cap_drop:[ALL] to exactly 4 leaves with the dashboard exempt — the pre-#255 posture. Now all 5 leaves drop caps (the dashboard runs non-root and owns its volume), so update the count and add a guard pinning tari's compose-set non-root user:. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#255): seed /data ownership in the dashboard image for named volumes The integration mini-stack mounts the dashboard's /data as a NAMED volume, not a host bind-mount, so pithead's ownership chown never runs and the non-root dashboard (uid 1000) couldn't write its SQLite DB → the container exited and every supervisor scenario failed. Create /data + /clearnet-state in the image owned by pithead. Docker seeds a fresh named volume from the image dir's ownership, so /data is writable in the mini-stack; with a host bind-mount (the real stack) the mount masks these and pithead's chown still governs. Verified locally: mini-stack 12 passed, 0 failed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#255): migrate root-owned data CONTENTS, not just the top-level dir ensure_owner checked only the data dir's own uid, but an install upgraded from the root-container era has a user-owned dir with root-owned *contents* — the daemons wrote bitmonero.conf, the dashboard SQLite DB, etc. as root. The non-root container can't overwrite those, so monerod/dashboard would fail to start even though the migration "ran". Scan the tree (find ! -uid, -quit on first foreign inode so a clean dir stays a fast no-sudo check) and chown -R when anything foreign is found. Found while deploying to the gouda bench. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…rity features (#322) #206 tracked three already-unit-tested security features whose end-to-end validation was explicitly deferred. Close each at its lowest honest tier and wire it into the gate, rather than forcing all three into the (marginal) tier-4 live matrix: - Public-IP stratum warning (#113) → tier-1 shell. The `is_public_ip` classifier was tested; the `check_stratum_exposure` warning composition (bind × public-IP × setup/doctor mode) was not. Added six cases that shadow `ip` with a stub so the host's real interfaces never decide the outcome. - Worker-SSRF guard (#122) → tier-1. `_safe_probe_host`/`get_stats` are covered, but the real injection vector — a miner-controlled `/workers` row (name@0, ip@1) flowing through `_normalize_proxy_workers` into the probe — wasn't asserted end-to-end. Added the tier-1 mirror of the deferred live test: an internal/loopback/bridge/multicast IP or a name-as-host is never probed; only the real miner IP is. - XvB stats + auto-register over Tor → tier-4 live (release gate). Added `assert_xvb_over_tor` to the `--check` read path: the running dashboard is wired to the Tor SOCKS (`XVB_TOR_PROXY=socks5h://<prefix>.25:9050`), which — together with the no-clearnet egress gate run alongside — proves the XvB call to xmrvsbeast can only have left via Tor. Regenerated docs/test-inventory.md (587→590 pytest cases, new shell section). Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* test: make dashboard frontend fixture deterministic + resync to build_state The committed tests/frontend/fixtures/state.json had drifted from build_state(): each worker object was missing the api_ok field the server now emits, so the fixture no longer mirrored the contract it documents. A naive regen was diff-noisy because _gen_state.py stamped time-relative values (now-relative chart x-timestamps, wall-clock last_update) plus a live socket lookup for host_addr. Pin them all — frozen NOW + UTC, patched views.time.time, and stubbed HOST_IP/detect_host_ipv4 — so regeneration is reproducible and diff-clean on any machine. Regenerated the fixture; the only payload changes are api_ok and the now-fixed timestamps. node --test tests/frontend/*.test.mjs: 61 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix: hoist views import to satisfy ruff E402 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…surface it (#321) * fix(#311): guard XvB controller against stale stats fetch When the xmrvsbeast.com stats fetch goes quiet (e.g. a stuck Tor circuit), `avg_1h` freezes at the last-good value. `fail_count` does NOT catch this — it is XvB's own reported count parsed from the page, not a fetch-failure counter — so the existing `fail_count >= 3` guard never fires and the controller keeps steering off a frozen number it can't refresh. Live on pithead-prod this drove ~55% of the fleet to XvB against a target it could no longer see, losing yield on both sides (less p2pool, no XvB credit). Treat a stale read as a first-class state, keyed off `last_update` (which bumps ONLY on a genuine fetch, #136): - _advance_controller is skipped when stale, so the loop holds the last split instead of winding the fraction up on a frozen avg_1h. - _smart_sleep's under-tier early-exit (the dominant cause of the over-donation — it cut every p2pool dwell short) is suppressed when stale. The legitimately computed XVB/SPLIT decision still ends the dwell. Cold start (last_update == 0, never fetched) is explicitly NOT stale, so the feedforward ramp to tier is untouched. Threshold XVB_STATS_STALE_AFTER_S defaults to ~3 fetch intervals. Tests cover the edge cases: predicate (cold-start / fresh / old), stale read holds the fraction vs. fresh read still ramps (#9/#70 catch-up preserved), and _smart_sleep holds the dwell on a stale below-tier read but still bails on a fresh one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#311): surface stale XvB stats in the dashboard Cosmetic half of the stale-fetch fix: when the xmrvsbeast.com fetch goes quiet the credited 1h/24h figures freeze, so the XvB Donation Stats card now greys them (status-warn + ⚠ on the label) and flips the footer from "Stats fetched (Updated: …)" to "⚠ Stale — last successful fetch: …", with a tooltip explaining the figures are frozen and the controller is holding its split. Routed figures (our own proxy history) are unaffected. To keep the controller and the UI from ever disagreeing about what "stale" means, the predicate is extracted to a single shared helper `utils.xvb_stats_are_stale`; AlgoService._stats_are_stale and the new Metrics.xvb_stale flag both call it. The flag is only raised when XvB is enabled. Tests: shared-predicate unit tests (cold-start / fresh / old / boundary), metrics flag tests (enabled+stale, disabled, cold-start), and a frontend render test asserting the greying + footer flip appear only when stale. StatCard gains an optional title for the tooltip. Fixture gets the new xvb_stale key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#311): regenerate test inventory for stale-stats tests Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#311): cover the fetch-freeze precondition the staleness guard relies on Extract the inline XvB stats sync in DataService.run into a _sync_xvb_stats() method (sibling to _maybe_register_xvb) so the linchpin of the #311 fix is directly unit-testable without standing up a stack: a FAILED fetch (get_stats() -> None) must persist nothing, leaving last_update frozen so the controller and dashboard can detect the feed is stale. A regression to stamping-on-failure would silently disable the whole staleness guard, so it gets an explicit test. The storage half of the guarantee (last_update bumps only on a real fetch, #136) was already covered in test_storage_service; this closes the data_service half. No behaviour change — pure extraction. 100% patch coverage on changed lines. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * ci(#54): regenerate the test inventory in pre-commit docs/test-inventory.md is generated from the suites; CI already FAILS on drift (test-inventory-check in the shell job), but nothing regenerated it locally, so a test add/remove that forgot `make test-inventory` only surfaced as a red CI run. Add a pre-commit hook that regenerates it whenever a test source changes (delegating to `make test-inventory`, matching the other local make-backed hooks). Same auto-fix UX as ruff-format: the file is rewritten and the commit aborts so it gets re-staged. Static grep, no test run — cheap. CI keeps the check as the backstop for anyone not running pre-commit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: pre-v1.1 conservative code + test cleanup
Behavior-preserving cleanups surfaced by a pre-release review across the
dashboard, CLI, and tests. No functional changes.
- release.sh: fix user-facing typo — the publish-cancelled hint pointed at
a nonexistent `--resume-promote-less` flag (the real flag is
`--resume-promote`).
- pithead: drop a needless `cat` fork in compose_up_checked (`$(<file)`).
- dashboard collector/pools.py: get_stratum_stats() built a worker_configs
list no caller used (workers are sourced live from xmrig-proxy); return
the raw dict only. Update the single call site + the now-dead tests.
- dashboard collector/system.py: fix copy-paste comment ("disk" -> "memory").
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: dedup PPLNS-window math, AVX2 + prune helpers, close Tari channel
The "aggressive" follow-ups to the conservative pass — still behavior-preserving,
each covered by a test.
Dashboard:
- Extract the PPLNS-window math (block_time-from-pool-type + count-shares-in-window)
into utils.pplns_block_time / shares_in_pplns_window, shared by metrics (display),
algo_service (routing) and data_service (XvB auto-register eligibility). Removes the
three near-duplicate copies that each inlined the 30/10 block times and the 2160
window default (now DEFAULT_PPLNS_WINDOW). + unit tests for the helpers.
- data_service.run(): close the Tari gRPC channel in a finally on loop teardown so
graceful shutdown (cleanup_background_tasks cancels the task) releases it, mirroring
state_manager.close(). TariClient.close() previously had no production caller.
CLI:
- Add cpu_has_avx2() and collapse the two duplicated platform-aware AVX2 probes
(doctor + check_prerequisites) onto it.
- Add monero_prune_flag() (built on the existing config_bool, so an explicit
prune:false is honoured per #294) and route render_env + preflight_resources through
it instead of the duplicated `jq if != null` + 1/0 block. + shell unit test.
- Route the clearnet_initial_sync reads through config_bool for consistency with the
rest of the bool parsing (behaviour identical — default is false).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): restore patch coverage + test inventory
CI on the prior commit failed two gates:
- Patch coverage (diff-cover >=90%): the try/finally reindent of data_service.run()
pulled the entire (partly untested) loop body into the changed-line set, and the
deleted get_stratum_stats test left its one-line body uncovered. The close()-on-
shutdown wiring was the only reason for that reindent and the benefit is negligible
(a forever-loop the OS reclaims on exit), so revert it — TariClient.close() stays as
the tested lifecycle method it already was. Restore a small get_stratum_stats test.
- Shell test-inventory-check: regenerate docs/test-inventory.md for the new
monero_prune_flag + PPLNS-helper tests (make test-inventory).
Keeps every behaviour-preserving refactor (PPLNS dedup, AVX2, prune flag, clearnet
config_bool); only the close() wiring is dropped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: annotate the refactors we deliberately did NOT do
Inline ponytail: comments at each site so a future contributor (or agent) sees the
rationale where they'd otherwise attempt the change: safe_sed/sudo_sed kept separate,
normalize_bool vs config_bool kept distinct (#294), TariClient.close left unwired, and
the test _data/_state_mgr fixtures left duplicated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: second-pass cleanup — dead code + small CLI dedup
Behaviour-preserving findings from a second review pass.
Dashboard:
- config.py: delete six unused P2Pool block-time constants (BLOCK_PPLNS_WINDOW_*,
SECOND_PER_BLOCK_P2POOL_*) — orphaned when the PPLNS math moved to helper/utils.py.
- tari_client.py: drop the unused `session`/aiohttp constructor param (TariClient talks
gRPC, never HTTP); update the two call sites.
- metrics.py: drop the pointless `_LOCAL_MONERO_HOST = LOCAL_MONERO_HOST` rebind; fold
its comment into _monero_mode().
- storage_service.py: drop a redundant `key.startswith("xvb_")` guard — the SQL already
filters `WHERE key LIKE 'xvb_%'`.
CLI (pithead):
- reset_dashboard now brings p2pool/dashboard up via compose_up_checked, so a subnet
collision (#180) gets the same explainer + rc check as every other up-path.
- inject_service_configs reuses the clearnet_state_dir() helper instead of re-deriving it.
- Two `echo | cut` forks replaced with parameter expansion (`${TARI_ONION%%.*}`,
`${MONERO_WALLET:0:8}`).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(tests): update TariClient() call in the fakes contract test
The contract test (make test-fakes, under tests/integration/fakes/) also
constructed TariClient(MagicMock()); the dropped session param made it fail in CI.
Update both call sites + drop the now-unused MagicMock import. (My earlier grep was
scoped to build/dashboard and missed this suite.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor: drop stray breadcrumb comment in config.py
ponytail-review nit: a reader won't grep for the deleted PPLNS constants, so the
pointer comment is noise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: genericize personal/infra references (gouda, hardware, miner hosts) Make the public repo free of setup-specific identifiers: - Rename tests/integration/gouda-testbench-README.md -> testbench-README.md and rewrite it as a generic "provision your own reference test server" guide (drop the specific NVMe model/device nodes/fstab; keep the technical guidance). - e2e.sh: GOUDA_HOST -> BENCH_HOST and miner default dropped (no personal defaults; require BENCH_HOST/MINER_HOST, add a --bench flag + clear guards); on_gouda -> on_bench, wait_gouda_healthy -> wait_bench_healthy. - Genericize remaining "gouda" prose across the test harness, benchmarks, and the test-server/integration/release docs to "the test bench" / "the reference box"; generalize the hardware-migration notes; keep all commands, numbers, and results. - Drop "gouda" from code comments (metrics.py, test_views.py), the release pipeline header (release.sh, Makefile), and a CHANGELOG line. Example LAN IPs (192.168.x / 10.0.0.x) and "Tailscale" as a generic remote-access option are left as-is — they're standard documentation, not personal setup. The LICENSE copyright holder is left untouched (legal authorship, not infra). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(faq,#257): update the vs. Gupax comparison Gupax (now gupax-io/gupax) bundles more than P2Pool+XMRig: an optional local Monero node, a proxy for external miners, and an XvB hashrate-splitter. Re-verified against the upstream README on 2026-06-29. Lead the comparison with form factor (server stack vs desktop app) and the two clean differentiators (Tor-first, Tari merge-mining), and acknowledge the node/proxy/XvB overlap honestly. No fork-lineage claims; link the canonical repo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#258): edit AI-voice tells out of the README Voice pass, no factual changes: drop the reflexive 'no X, no Y' repeats (keep one deliberate use), the 'out of the box' / cutesy-triad marketing filler, and some decorative bold. Commands, flags, and numbers unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite CONTRIBUTING.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite SECURITY.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite build/dashboard/README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite build/dashboard/mining_dashboard/client/tari/Readme.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/architecture.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/benchmarks/tor-vs-clearnet.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/configuration.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/dashboard.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/faq.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/getting-started.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/hardware.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/integration-testing.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/operations.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/privacy.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/release-server.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/releasing.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/test-server-architecture.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/testing-guide.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/testing-strategy.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite docs/workers.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite images/launch/README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite tests/integration/README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: rewrite tests/integration/testbench-README.md for voice consistency Voice/style pass to match the cadence of the p2pool, xmrig, and monero READMEs. No technical facts, commands, flags, addresses, ports, or numbers changed (code fences and inline code verified byte-identical). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Bumps the docker group with 1 update in the /build/dashboard directory: python. Updates `python` from `ae52c5b` to `b27df58` --- updated-dependencies: - dependency-name: python dependency-version: 3.11-slim dependency-type: direct:production dependency-group: docker ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps the actions group with 1 update: [actions/setup-python](https://github.com/actions/setup-python). Updates `actions/setup-python` from 6.2.0 to 6.3.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](actions/setup-python@a309ff8...ece7cb0) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>
Bumps the python group in /build/dashboard with 1 update: [ruff](https://github.com/astral-sh/ruff). Updates `ruff` from 0.15.18 to 0.15.20 - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](astral-sh/ruff@0.15.18...0.15.20) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.15.20 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: python ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vijit Singh <vijit.n.singh@gmail.com>
config.json.template -> config.minimal.json (the bare-minimum starter: just the two payout addresses, copied to config.json) config.advanced.example.json -> config.reference.json (the full reference: every key with its default value) The names now say what each file is. Content is unchanged — the reference already documents every config key (verified against docs/configuration.md), and the minimal file is still just the wallet addresses. Updated all references (CLI text, README, getting-started, configuration, releasing, the release bundle, and the bundle test). CHANGELOG entries keep the old names — those releases really did ship them. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…330) * test: cover flagged edge cases + fix snapshot persistence-health bug Source-vs-tests audit of the four testing tiers. Adds coverage at the lowest honest tier (all tier 1) for branches with no covering test, and fixes one real bug found along the way. Bug fix: - storage_service.save_snapshot: a non-serializable snapshot (TypeError from json.dumps) logged the error but left db_healthy=True, so the #131 "DB write failing" badge never fired while snapshots silently stopped persisting. Route it through _db_error like every other write path. Tests added (tier 1): - pytest: unserializable snapshot flips db_healthy (regression guard for above). - pithead shell: firewall install-failure rollback (#270); wallet subaddress / integrated hard-fail (#250); remote-mode-without-host and non-/24 subnet guards (#180); ensure_owner conditional + whole-tree recursive chown (#255). - frontend: ProxyTotals high-reject styling; per-worker api/reject badges + XvB/Unknown pool badges; the #278/#313 Tari-✔ invariant (never on active-but-dead); Gauge done-vs-syncing. Docs: - test-inventory regenerated (764 -> 770 cases); testing-strategy Known-gaps gains a "Coverage-audit follow-ups" section listing the tier-3/4 backstops still needed (real-kernel firewall rollback, real mixed-ownership ensure_owner, real-container monerod failover, non-blocking-Tari ignore, busy/reorg, double-outage, doctor NTP check, ENOSPC, Tor-down, insecure+main row). make lint + make test green; dashboard 94.73% cov; patch coverage 100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(integration): add tier-4 backstops for firewall rollback + ensure_owner The two divergence risks the audit flagged — paths PR #330 proves only with stubs — now have live counterparts, folded into the existing opt-in phases (no new flags), following the --auth-fail-closed "live counterpart to a tier-1 assertion" pattern. - --fault-injection: fault_firewall_rollback shadows iptables with a wrapper that fails every `-I`, re-runs apply_tor_egress_firewall, and asserts the box ends fail-closed (no pithead-tor-egress rule left half-installed), then reinstates the real firewall. Belt-and-braces reinstates unconditionally so a mid-phase abort can't leave clearnet egress open. Proves the real-kernel strip the tier-1 stub can't (#270). - --lifecycle: plants a root-owned file under the dashboard data dir and asserts the pool-flip apply (ensure_directories -> ensure_owner) chowns it to uid 1000 — the #255 "scan contents, not just the dir" regression, which needs root to set up so it can't live at tier 1. Both are DESTRUCTIVE-then-restored, local-box only, and run at the release gate (not CI). Help text, testing-strategy Known-gaps, and the test inventory updated. Verified: shellcheck + shfmt clean, bash -n parses, integration self-test 97/0, make lint green, inventory drift-check green. NOT executed against a real box — that happens at the release gate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(integration): fix firewall-rollback sabotage — shadow sudo, not iptables Validated the two tier-4 backstops live on a real box (gouda). The ensure_owner migration passed as written. The firewall-rollback check did NOT exercise the rollback: apply_tor_egress_firewall calls `sudo iptables -I`, and sudo's secure_path ignores a PATH-shadowed iptables, so the real insert succeeded and the rollback branch never ran (the rc-0 assertion passed for the wrong reason — a false green). Fix: shadow `sudo` instead of `iptables`. sudo is still resolved via PATH, so a wrapper that fails an `iptables … -I …` insert and execs real sudo for everything else (remove's -D, -N, iptables-save) makes the insert fail exactly as a real mid-insert error would. Re-validated live: with the real -I failing, the partial ruleset is rolled back (0 pithead-tor-egress rules), then reinstated (7). Box restored byte-identical to its pre-test iptables snapshot. shellcheck + shfmt clean, bash -n parses, self-test 97/0, inventory drift green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Collaborator
Author
|
✅ Full destructive tier-4 matrix — green. ``` Ran live on gouda ( Release gate satisfied. Cutting via |
Bump VERSION 1.0.3 -> 1.1.0 (dashboard pyproject in lockstep) and add the curated CHANGELOG entry for the v1.1 privacy release. Minor bump: develop carries 9 feat commits and behavior changes on top of v1.0.3 (Tor-by-default egress, non-root containers, XvB auto-registration, Stack Topology panel, supply-chain hardening) — new backward-compatible functionality, so MINOR per SemVer. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
87a3aa6 to
78404e4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cuts Pithead v1.1.0 — the first minor on top of the 1.0.x line.
Why 1.1.0 (not 1.0.4)
developis 43 commits ahead of releasedv1.0.3, including 9feat:commits and behavior/default changes — new backward-compatible functionality → MINOR per SemVer. A patch would only be right for a fix-only delta, which this is not.What's in it
Full curated notes in
CHANGELOG.md.Changes in this PR
VERSION1.0.3 → 1.1.0build/dashboard/pyproject.tomlin lockstep (drift-guard)CHANGELOG.md1.1.0 sectionE2e validation (tier-4, live on gouda, develop @ f1e6db2)
--safety-backup --lifecycle): running; result posted below.Release is cut from
mainafter merge viascripts/release.shon gouda.🤖 Generated with Claude Code