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
8 changes: 8 additions & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ useDefault = true
[allowlist]
description = "Accepted false positives"
regexTarget = "line"
# Archived third-party source pages in the research record (checksummed, reviewed on archive):
# hex example IDs in the p2pool.observer API docs and page-chrome tokens in saved GitHub HTML
# pattern-match crypto-token rules. The archives must stay byte-identical to their recorded
# checksums, so the redaction that would normally fix a finding is impossible by design.
paths = [
# both path forms exist in history: pre-move (research/...) and current (docs/research/...)
'''(docs/)?research/xvb-delivery-study/data/sources/.*''',
]
# curl auth assembled from shell ENV VARS (e.g. `-u "${USER:-}:${PASS:-wallet}"`) is not a hardcoded
# secret — the upstream `curl-auth-user` rule can't distinguish `${VAR}` from a literal credential.
# Both sides must be `${VAR:-default}` expansions (default optional), so a real `-u "admin:hunter2"`
Expand Down
2 changes: 1 addition & 1 deletion .markdownlint-cli2.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@
"**/.venv/**",
"build/dashboard/mining_dashboard/client/tari/generated/**",
"docs/dev/test-inventory.md", // generated by `make test-inventory`; not hand-edited
"research/**" // verbatim research records (numbered audit trails, quoted material) — reviewed as research, not house prose
"docs/research/**" // verbatim research records (numbered audit trails, quoted material) — reviewed as research, not house prose
]
}
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ runs `ruff` (plus a few hygiene hooks) on your changed files. If you change depe
## Development workflow

1. Fork the repo and create a branch off `develop` (the integration branch; `main` holds released
commits only).
commits only — how releases move `develop` → `main` and back is in
[Releasing › Branch mechanics](docs/dev/releasing.md#branch-mechanics)).
2. Make your change. Keep it focused: one logical change per PR.
3. Run the full test suite locally:

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ lint: lint-sh lint-py lint-js lint-yaml lint-md lint-docs-voice lint-operator-st
lint-sh: ## shellcheck + shfmt over the CLI, build/* container scripts, release + test scripts
shellcheck --severity=warning pithead pithead-completion.bash scripts/*.sh build/*/*.sh tests/stack/run.sh tests/stack/test_compose.sh \
tests/inventory.sh tests/integration/*.sh tests/integration/mini-stack/*.sh
shfmt -i 4 -d pithead pithead-completion.bash $(shell git ls-files '*.sh' | grep -v '^research/')
shfmt -i 4 -d pithead pithead-completion.bash $(shell git ls-files '*.sh' | grep -v '^docs/research/')

lint-py: ## ruff lint + format check on all repo Python (ruff runs via uv from the locked dev extra)
uv run --locked --project build/dashboard --extra dev ruff check .
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ a Tor daemon. The `pithead` script renders config, provisions Tor, and drives do
[The Dashboard](docs/dashboard.md).
- ⚙️ **One config, tuned to your setup.** A local or remote Monero node, pruned or full; the P2Pool
tier (`main`, `mini`, or `nano`); XvB donation strategy; per-worker power and API settings; four
alert channels; timezone, memory limits, and every privacy toggle — around 94 keys across 13
alert channels; timezone, memory limits, and every privacy toggle — around 113 keys across 13
sections, all in one `config.json` and validated on every `apply`. Most have defaults you'll never
touch. See [Configuration](docs/configuration.md).
- 💡 **Energy-aware earnings.** Set your electricity cost and coin prices — typed in, or fetched
Expand Down Expand Up @@ -119,7 +119,7 @@ Full walkthrough: [docs/getting-started.md](docs/getting-started.md)
| **[Configuration](docs/configuration.md)** | Every `config.json` key, applying changes safely, reusing an existing node, and remote Monero nodes. |
| **[The Dashboard](docs/dashboard.md)** | Sync Mode, a tour of the live operational view, and the opt-in control channel: editing config, one-click upgrades, and the audit logs from the browser. |
| **[Connecting Miners](docs/workers.md)** | Point any existing rig at the stack, or spin up a tuned miner with [RigForge](https://github.com/p2pool-starter-stack/rigforge). |
| **[Architecture](docs/architecture.md)** | The nine services, the privacy model, and the algorithmic XvB switching engine. |
| **[Architecture](docs/architecture.md)** | The eleven services, the privacy model, and the algorithmic XvB switching engine. |
| **[Privacy & Network Egress](docs/privacy.md)** | Every off-box connection: what's Tor-routed, what's clearnet today, and how to harden it. |
| **[Operations & Maintenance](docs/operations.md)** | Full command reference, upgrades, backups, and troubleshooting. |

Expand All @@ -129,10 +129,10 @@ Browse the full index at **[docs/](docs/README.md)**.

## 🏗️ How it works

The stack orchestrates nine services via Docker Compose: a Monero full node, P2Pool, a Tari base
The stack orchestrates eleven services via Docker Compose: a Monero full node, P2Pool, a Tari base
node, an XMRig proxy (your single worker endpoint), Tor for anonymity, the dashboard plus switching
engine, a read-only Docker socket proxy (plus a tiny start/stop-only control proxy), and Caddy for
HTTPS.
engine, a read-only Docker socket proxy (plus a tiny start/stop-only control proxy), Caddy for
HTTPS, and two opt-in view-only wallets that confirm payouts on-chain.

```mermaid
flowchart TB
Expand Down
39 changes: 0 additions & 39 deletions build/dashboard/mining_dashboard/client/xmrig_proxy_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging

import requests
Expand Down Expand Up @@ -166,41 +165,3 @@ def update_config(self, config_data):
if response.status_code == 204 or not response.content:
return {}
return response.json()


if __name__ == "__main__":
# Configuration
# Ensure xmrig-proxy is running with API enabled:
# ./xmrig-proxy --http-port=8080 --http-access-token=SECRET

HOST = "127.0.0.1"
PORT = 8080
TOKEN = "SECRET" # noqa: S105 — placeholder for this __main__ usage example, not a real secret

client = XMRigProxyClient(HOST, PORT, TOKEN)

try:
# 1. Get Summary
print("--- Summary ---")
summary = client.get_summary()
print(json.dumps(summary, indent=4))

# 2. Get Workers
print("\n--- Worker Details ---")
workers = client.get_workers()
print(json.dumps(workers, indent=4))

# 3. Get Config
print("\n--- Current Config ---")
config = client.get_config()
print(json.dumps(config, indent=4))

# 4. Update Config (Example: changing donate level)
# print("\n--- Updating Config ---")
# updated_config = client.update_config({"donate-level": 1})
# print(json.dumps(updated_config, indent=4))

except requests.exceptions.RequestException as e:
print(f"HTTP Request failed: {e}")
except Exception as e:
print(f"An error occurred: {e}")
39 changes: 14 additions & 25 deletions build/dashboard/mining_dashboard/helper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,42 +94,31 @@ def format_disk_size(used_gb, total_gb):
return f"{used:.1f}", f"{total:.1f}", "GB"


def format_xmr(amount):
"""Format an XMR amount with magnitude-adaptive precision — 4 decimal places at >= 1 XMR,
6 at >= 0.001, 8 below that — so a small daily estimate isn't truncated to zeros.

Mirrors ``formatXmr`` in ``web/static/logic.mjs`` (the dashboard earnings card) so the same
estimate reads the same on every surface (#387). Returns "0 XMR" for zero and an em dash for
non-numeric input.
"""
def _format_coin(amount, symbol):
"""Magnitude-adaptive coin formatting — 4 decimal places at >= 1, 6 at >= 0.001, 8 below —
so a small daily estimate isn't truncated to zeros. Mirrors ``formatXmr``/``formatXtm`` in
``web/static/logic.mjs`` so the same estimate reads the same on every surface (#387).
Returns "0 <symbol>" for zero and an em dash for non-numeric input."""
try:
val = float(amount)
except (ValueError, TypeError):
return "—"
if not math.isfinite(val):
return "—"
if val == 0:
return "0 XMR"
return f"0 {symbol}"
dp = 4 if val >= 1 else 6 if val >= 0.001 else 8
return f"{val:.{dp}f} XMR"
return f"{val:.{dp}f} {symbol}"


def format_xtm(amount):
"""Format an XTM amount, the Tari sibling of :func:`format_xmr` — same magnitude-adaptive
precision, same "0 XTM" / em-dash edge cases.
def format_xmr(amount):
"""XMR face of :func:`_format_coin`."""
return _format_coin(amount, "XMR")

Mirrors ``formatXtm`` in ``web/static/logic.mjs`` so a confirmed Tari total reads identically
in the bot and on the dashboard card (#387)."""
try:
val = float(amount)
except (ValueError, TypeError):
return "—"
if not math.isfinite(val):
return "—"
if val == 0:
return "0 XTM"
dp = 4 if val >= 1 else 6 if val >= 0.001 else 8
return f"{val:.{dp}f} XTM"

def format_xtm(amount):
"""XTM (Tari) face of :func:`_format_coin`."""
return _format_coin(amount, "XTM")


def format_duration(seconds):
Expand Down
11 changes: 9 additions & 2 deletions build/dashboard/mining_dashboard/service/algo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,10 +427,17 @@ def _dwell_should_end(
# effective split far past the computed fraction. A *changed* decision
# still ends the dwell — only the avg-driven override pauses.
target_hr = self._get_target_donation_hr(stable_hr)
# Catch up only toward what the donation cap allows (#898). Against an explicit target
# the fleet cannot sustain, the raw target comparison reads avg_1h as permanently
# "under tier", ends every p2pool dwell at its first check tick, and the actuated
# donation pins near 100% of allowed time — the cap never binds (measured live:
# ~94% of the fleet routed under a 0.65 cap). The achievable donation is the ceiling
# worth catching up to; beyond it, cutting dwells short only burns the p2pool side.
achievable_hr = min(target_hr, stable_hr * self.max_donation_fraction)
under_tier = (
not self._stats_are_stale(xvb_stats)
and target_hr > 0
and xvb_stats.get("avg_1h", 0) < target_hr
and achievable_hr > 0
and xvb_stats.get("avg_1h", 0) < achievable_hr
)
return decision != held_decision or under_tier

Expand Down
2 changes: 1 addition & 1 deletion build/dashboard/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ line-length = 100
# Generated Tari gRPC stubs aren't ours to style — already omitted from coverage too.
extend-exclude = [
"mining_dashboard/client/tari/generated",
"../../research",
"../../docs/research",
] # research/: verbatim study-record scripts, kept byte-identical to the archived raw copy

[tool.ruff.lint]
Expand Down
44 changes: 44 additions & 0 deletions build/dashboard/tests/service/test_algo_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,50 @@ async def test_xvb_tor_opt_out_no_socks5(self, algo):
assert all("socks5" not in p for p in pools)


class TestDwellCatchUpCap:
"""#898: the under-tier catch-up must chase the ACHIEVABLE donation (stable_hr x cap), not
the raw tier target — an unreachable explicit target otherwise ends every p2pool dwell at
its first check tick and the cap never binds (measured live: ~94% routed under a 0.65 cap)."""

def _fresh(self, avg_1h):
return {"avg_1h": avg_1h, "avg_24h": avg_1h, "last_update": time.time()}

def test_unreachable_target_does_not_end_dwell_once_cap_is_met(self, algo):
# mega target on a 268k fleet with cap 0.65: achievable = 174.2k. Credited 200k is
# under the 1M target but ABOVE achievable — the dwell must run its course.
algo.donation_level = "mega"
algo.max_donation_fraction = 0.65
with patch.object(algo, "get_decision", return_value=("SPLIT", 1000)):
assert (
algo._dwell_should_end("SPLIT", 268_000, 268_000, {}, {}, self._fresh(200_000), [])
is False
)

def test_catch_up_still_fires_below_the_achievable_ceiling(self, algo):
algo.donation_level = "mega"
algo.max_donation_fraction = 0.65
with patch.object(algo, "get_decision", return_value=("SPLIT", 1000)):
assert (
algo._dwell_should_end("SPLIT", 268_000, 268_000, {}, {}, self._fresh(150_000), [])
is True
)

def test_reachable_target_keeps_the_original_behaviour(self, algo):
# whale on the same fleet: target 100k < achievable 174.2k — the raw-tier comparison
# is unchanged (under 100k ends the dwell, above it does not).
algo.donation_level = "whale"
algo.max_donation_fraction = 0.65
with patch.object(algo, "get_decision", return_value=("SPLIT", 1000)):
assert (
algo._dwell_should_end("SPLIT", 268_000, 268_000, {}, {}, self._fresh(90_000), [])
is True
)
assert (
algo._dwell_should_end("SPLIT", 268_000, 268_000, {}, {}, self._fresh(105_000), [])
is False
)


class TestSmartSleep:
LATEST = {"total_live_h15": 15_000, "total_live_h10": 15_000, "pool": {}, "shares": []}

Expand Down
7 changes: 6 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ stack. The other guides cover individual topics once you're running.
| [Monitoring & Alerting](monitoring.md) | Optional Healthchecks.io dead-man's switch — get alerted when your host goes down (power loss, crash), even when it can't tell you itself — plus the Prometheus `/metrics` endpoint for Grafana or any scraper. |
| [Telegram Bot](telegram.md) | Push operator alerts (node down/recovered, worker offline/back, sync finished) to Telegram and query stack status on demand (`/status`, `/hashrate`, `/workers`, `/sync`) — creating a bot, finding your chat id, per-event toggles, and the command list. |
| [Connecting Miners](workers.md) | Pointing any existing rig at the stack, plus [RigForge](https://github.com/p2pool-starter-stack/rigforge) for setting up new miners. |
| [Architecture](architecture.md) | The nine services, how they fit together, the privacy model, and the algorithmic XvB switching engine. |
| [Architecture](architecture.md) | The eleven services, how they fit together, the privacy model, and the algorithmic XvB switching engine. |
| [Privacy & Network Egress](privacy.md) | Every connection the stack makes off-box: what's Tor-routed, what's clearnet today, and how to harden each path. |
| [Operations & Maintenance](operations.md) | The full `pithead` command reference (including command chaining and tab-completion), upgrades, encrypted backups, rotating the internal secrets, watching for intruders, and troubleshooting. |
| [FAQ](faq.md) | Common questions, plus why Pithead vs. doing it yourself or Gupax. |
Expand All @@ -35,6 +35,11 @@ Docs for working on Pithead itself live in [`dev/`](dev/). Operators don't need
| [Releasing](dev/releasing.md) | How Pithead is versioned and released: one product, one version, the `VERSION` source of truth, and the GHCR stage→promote pipeline. |
| [Release / Validation Server](dev/release-server.md) | Why end-to-end validation needs a dedicated server (and what GitHub Actions does free on every PR), how to provision and harden it, and the safe self-hosted-runner setup. |
| [Test Server Architecture](dev/test-server-architecture.md) | How the reference test/build box is structured and how to recreate it. |
| [Dual-distribution plan](dev/dual-distribution-plan.md) | The ratified #77/#78 architecture decision record: how one release ships as the Compose stack, the flashable appliance image, and a git clone — and the rejected alternatives. |

Published research lives in [`research/`](research/): the
[XvB delivery study](research/xvb-delivery-study/PAPER.md) is the measured evidence behind the
dashboard's XvB "Study est." figures.

## Quick links

Expand Down
6 changes: 4 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Architecture

The stack runs nine containerized services under Docker Compose. This doc lists each service and how
they connect.
The stack runs eleven containerized services under Docker Compose — two of them opt-in view-only
wallets for payout confirmation. This doc lists each service and how they connect.

The services provide a Monero full node, P2Pool sidechain mining, Tari merge-mining, a single worker
endpoint, and a monitoring dashboard. All node P2P and transaction traffic routes over Tor; no public
Expand All @@ -20,6 +20,8 @@ port forwarding is required.
| 7 | **Docker Proxy** | A **read-only** proxy onto the Docker socket so the dashboard can read container stats/logs — no write access. |
| 8 | **Docker Control** | A second, minimal socket proxy scoped to **only** `start`/`stop` (nothing else — not create/kill/exec/reads), so the dashboard can reject workers when a node is down (Issue #31), hold p2pool + xmrig-proxy until the chains finish syncing (Issue #35), switch a clearnet-syncing node back to Tor once it's synced (Issue #234), and, opt-in via `dashboard.fail_closed`, hold p2pool + xmrig-proxy again on an unrecoverable dashboard health failure (Issue #490). Kept separate so its write grant can't widen the read-only proxy. |
| 9 | **Caddy** | A reverse proxy that serves the dashboard over HTTPS (automatic local TLS) on the LAN. |
| 10 | **Monero Wallet-RPC** | Opt-in: runs only when `monero.view_key` is set (compose profile `payout_confirm`). A view-only `monero-wallet-rpc` against the local node, so the dashboard can confirm P2Pool payouts on-chain. See [Dashboard › Payout confirmation](dashboard.md#payout-confirmation). |
| 11 | **Tari Console Wallet** | Opt-in: runs only when `tari.view_key` is set (compose profile `tari_payout_confirm`). A view-only `minotari_console_wallet` against the local Tari node, confirming merge-mine payouts on-chain. See [Dashboard › Payout confirmation](dashboard.md#payout-confirmation). |

## High-level diagram

Expand Down
Loading