Skip to content

Latest commit

 

History

History
227 lines (186 loc) · 15.3 KB

File metadata and controls

227 lines (186 loc) · 15.3 KB

Benchmark: mining over Tor vs clearnet (#256)

This document records a benchmark of mining-path yield over Tor versus clearnet: what we measured, from where, and how we decided.

Status: complete (2026-06-22). Routing p2pool over Tor costs ≈10 % of mining yield on mini. The cost is propagation latency (uncle/late shares), not rejects, and Tor stays the default. Full per-arm results and the committed raw data are in Results. The methodology and decision rule below were fixed before the run (pre-registration); the run executed on the test bench.

The question

While the stack is actively mining at steady state, does routing the mining-path networking over Tor instead of clearnet measurably cost yield or reliability? This gates the Tor-by-default decision for two paths:

  • p2pool outbound sidechain P2P (#165) — p2pool.clearnet knob.
  • XvB donation mining (#166) — xvb.tor knob.

This is not about initial sync over Tor. That is a separate, already-settled question (#183/#234: Tor IBD is impractically slow, hence the opt-in clearnet-then-Tor initial sync). Here both monerod and Tari stay on Tor throughout; only the p2pool / XvB egress changes between arms.

What we measure, and from where

Every source below was verified against the live p2pool container on the test bench (/stats/* files written by p2pool's --local-api / --data-api, plus the dashboard /api/state and docker stats).

Primary — yield (the bottom line)

"Does Tor cost us money" is captured directly by our share of the PPLNS reward window. No raw orphan counter is required: an orphaned/uncled share fails to earn its full weight, which shows up here.

Metric Meaning Source · field
PPLNS reward share our % of the sidechain reward window — direct revenue /stats/local/stratum · block_reward_share_percent
Yield efficiency reward-share ÷ hashrate-share; < 1 ⇒ shares going stale derived: block_reward_share_percent ÷ ( hashrate_1h ÷ /stats/pool/stats · pool_statistics.hashRate )
Effort hashes-per-share vs expected — rises when shares are found but not counted /stats/local/stratum · average_effort, current_effort
Uncle / orphan events (our shares) raw stale-share count — a mechanism cross-check on the yield number parse the p2pool console log (SHARE FOUND + SideChain uncle markers); not in /api/state

Why reward-share is primary, not a raw orphan count. p2pool includes uncle shares at reduced weight and drops orphans entirely; both effects land in block_reward_share_percent, so the net revenue impact is measurable from the stratum stats today. The raw uncle count (log-parse) is kept as a secondary cross-check, not the decision metric. It is noisier and harder to attribute to our shares.

Secondary — the mechanism (latency / connectivity)

These explain why the yield moved, or confirm it didn't. All are available today.

Metric Source · field
Share-found cadence / time-to-first-share /stats/local/stratum · shares_found, last_share_found_time
Sidechain peer count & churn /stats/local/p2p · connections, incoming_connections, peer_list_size
Stratum-level rejects /stats/local/stratum · shares_failed
Effective hashrate on pool /stats/local/stratum · hashrate_1h, hashrate_24h
Tor daemon overhead under sustained traffic docker stats tor (CPU %, mem)

XvB donation path (#166)

Not collected in the headline run. XvB is disabled so the p2pool-yield comparison isn't confounded (see Method below). These fields apply only to a separate, dedicated XvB-transport measurement, where they'd be sampled during donation windows (when algo_service has routed the proxy to XvB).

Metric Source · field
Accepted / rejected / invalid shares to XvB xmrig-proxy /summary/api/state · proxy_summary.{accepted,rejected,invalid}
Credited raffle weight XvB stats API (already fetched over Tor, #163) · block_reward_share_percent analogue at XvB

Method

  • Rig: the test bench (running develop) plus the full RigForge fleet (8 miners, ~269 kH/s combined) at a fixed hashrate, all pointed at the bench's stratum. A constant total hashrate across both arms is load-bearing for the ratio metric below.
  • Sidechain: mini. At ~269 kH/s we are ~1.8 % of the mini sidechain (observed ~14.75 MH/s), or ~155 of our own shares/day. That gives enough statistical power while staying a small, representative participant. (main at this hashrate yields only ~10 shares/day, too sparse. nano would make us ~11 % of the chain, large enough that our own latency would drag it and overstate the Tor penalty. The uncle/orphan rate is set by the ~10 s share interval, the same on every sidechain, so the mini result generalises to main/nano.)
  • Design — INTERLEAVED A/B (not one long block per arm): alternate T → C → T → C in fixed 20 h blocks so each arm samples the same diurnal / weekly / network-weather range. (A "5 days Tor then 5 days clearnet" layout would confound the arm with the calendar period.) Discard the first 3 h after each switch while p2pool reconnects and the PPLNS window re-stabilises. As run: 6 blocks, 3 per arm, over ~5 days, yielding ~270–290 of our shares per arm. That is below the ideal ≥500, a deliberately shortened schedule, which is why the result is reported as "roughly 10 %" rather than a tight point estimate (see the confidence note in Results).
    • Arm T (Tor): p2pool.clearnet=false (--socks5), tor_egress_firewall=true, xvb.tor=true — the develop defaults (fail-closed, egress-gated).
    • Arm C (clearnet): p2pool.clearnet=true, tor_egress_firewall=false, xvb.tor=false. The firewall must be off in this arm. The #270 Tor-egress firewall DROPs direct clearnet dials, so leaving it on would give p2pool 0 sidechain peers and the arm would silently collect garbage. The arm switch toggles the two together. monerod + Tari keep their Tor app-config in both arms, so only p2pool's transport differs; clearnet exposure here is the intended baseline on the test bench.
    • Only the mining-path transport flips. Held constant in both arms: monerod + Tari on Tor, XvB disabled (xvb.enabled=false, see below), the rig, hashrate, sidechain, and monerod tip.
    • Why XvB is disabled for this run. With XVB_DONATION_LEVEL=auto the optimizer dynamically splits the fleet between p2pool and XvB to climb raffle tiers. On the test bench it routed ~96 kH/s to XvB vs only ~18 kH/s to p2pool while chasing the Whale tier. That (a) starves p2pool, so reward_share, our primary metric, would be measured on a fraction of the fleet, and (b) makes the split itself a function of Tor latency, i.e. the optimizer reacts to the very thing we're trying to isolate. Disabling XvB sends the full ~269 kH/s to p2pool in both arms, so reward_share is a clean readout of transport overhead (and ~155 shares/day, not a trickle). The XvB transport overhead (#166) is a separate, smaller question, benchmarked on its own, not folded into the p2pool-yield comparison. The harness re-asserts xvb.enabled=false on every pithead apply so a switch or recovery can't let the optimizer drift back on.
    • Every switch is egress-gated by bench-verify-egress.sh. Arm T must be a clean all-Tor PASS before any data is collected, so a leak can never silently invalidate a window.
  • Controlling natural variance: (1) interleave (the dominant control); (2) compare the ratio yield-efficiency = reward-share ÷ hashrate-share, which cancels out the sidechain's total hashrate drifting as other miners come and go; (3) run to a target share count, not a fixed clock, so Poisson noise (~1/√N) sits below the effect we're resolving; (4) the clearnet arm's block-to-block variance is the noise floor for the decision; (5) hold everything else constant.
  • Collector: bench-collect.sh, a read-only poller that snapshots the /stats/* fields plus docker stats tor into one JSONL line per interval (default 5 min), per arm, in ~/pithead-bench/<arm>.jsonl. No new container.

Running it — autonomous on the test bench

The run must survive the operator being offline for days, so everything runs on the test bench; a laptop / SSH session is only an optional observer. Three layers:

  • bench-orchestrate.sh (detached on the bench) drives the whole run: flips the arm each block via pithead apply, egress-gates the switch, (re)starts the collector, health-checks every cycle, writes ~/pithead-bench/status.json, and self-protects. Transient blips (a miner reconnecting, Tor circuit churn) are tolerated and recorded, but a genuine unrecoverable break pauses collection and marks that window invalid rather than polluting the dataset. An unwatched multi-day break then costs calendar time, not data.
  • Cron watchdog (*/15 * * * * plus an @reboot entry) runs bench-healthcheck.sh: restarts the orchestrator or collector if either died, re-ups the stack if it's down, appends health.log. This catches the orchestrator itself dying or a bench reboot, which the orchestrator can't self-report. Runs as the operator user; the only privileged step (iptables) is covered by the scoped /etc/sudoers.d/pithead-firewall grant.
  • Observe over Tailscale: ssh $BENCH_HOST bench-status (bench-status.sh) prints a one-screen summary: current arm, day, last-OK, shares per arm, egress verdict, miners, collector. The dashboard UI (on the bench's Tailscale IP) is the live stack glance.

Runbook

# 1. calibrate (~6–12 h, Tor arm): confirm live mini total, our share, shares/day
tests/integration/benchmarks/bench-orchestrate.sh --calibrate --pool mini

# 2. start the autonomous interleaved run + install the watchdog cron
#    (the exact invocation used for the run recorded in Results)
nohup tests/integration/benchmarks/bench-orchestrate.sh run \
      --pool mini --block-hours 20 --blocks 6 --settle-hours 3 --interval 300 >/dev/null 2>&1 &
tests/integration/benchmarks/bench-orchestrate.sh --install-cron   # */15 + @reboot watchdog

# 3. check from anywhere over Tailscale
ssh $BENCH_HOST bench-status
ssh $BENCH_HOST tail -n 40 pithead-bench/health.log

# 4. graceful stop (restores the Tor default + egress firewall + re-enables XvB, removes the cron, keeps the JSONL)
tests/integration/benchmarks/bench-orchestrate.sh --stop

Decision rule

The clearnet arm establishes the noise floor (its own variance across sub-windows). Then:

  • Tor yield-efficiency / reward-share delta within that noise floor ⇒ keep Tor as the default for that path (#165/#166 ship as-is); document the measured (negligible) trade-off.
  • Tor materially below clearnet (a reward-share loss clearly outside the noise, especially if concentrated on --mini/--nano) ⇒ document the trade-off and reconsider: a per-sidechain default, or clearnet-default with a Tor opt-in, rather than a blanket Tor default.

Either way the conclusion lands in docs/privacy.md so operators get an honest trade-off, and it sets the final default for #165/#166 before the v1.1 release.

Results

Run: mini, 2026-06-17 → 2026-06-22 (~5 days), 6 × 20h blocks interleaved T/C/T/C/T/C (3 per arm), first 3h of each block discarded. Full 8-miner fleet ≈ 269 kH/s held constant, XvB disabled so the whole fleet hit p2pool in both arms. The sidechain grew from ~14.7 to ~20.9 MH/s over the run (our share drifted ~1.8 % → ~1.3 %), which is why the yield-efficiency ratio (reward ÷ hashrate share) is the headline metric, not raw reward share. ~1,220 post-settle snapshots total. Reproduce with python3 tests/integration/benchmarks/bench-analyze.py docs/benchmarks/data/run-2026-06-17-mini; the raw per-arm JSONL plus events.log are committed there (originals on the test bench at ~/pithead-bench/).

Per-arm (mean of the 3 block means; ± is the block-to-block stdev, the noise floor):

Arm Blocks Reward share % Yield-efficiency Avg effort % Shares Rejects Peers Tor-daemon CPU
clearnet 3 1.375 ± 0.132 1.054 ± 0.085 89.9 292 0 10.0 19.6 %
Tor 3 1.218 ± 0.157 0.933 ± 0.098 112.9 270 0 10.2 18.7 %
Δ (Tor vs clearnet) −11.4 % −11.5 % +23 pts −7.5 % ≈0 ≈0

Per-block (transparency; note the ranges overlap):

Blk Arm Reward % Yield-eff Effort % Shares
1 Tor 1.396 0.860 111.8 107
2 clearnet 1.543 0.969 95.6 111
3 Tor 1.013 0.869 125.0 76
4 clearnet 1.363 1.170 86.3 94
5 Tor 1.245 1.071 101.9 87
6 clearnet 1.220 1.024 87.7 87

Findings:

  1. Tor costs ≈ 10–12 % of p2pool yield on mini. Reward share and yield-efficiency agree (−11.4 % / −11.5 %), and the direction is consistent across the run.
  2. The cost is propagation latency, not anything else. Rejects were 0 in both arms, the Tor daemon used the same CPU in both (~19 %, since monerod + Tari route over Tor regardless of arm), and peer counts matched (~10). The only moving part is share-propagation delay over Tor, which shows up as higher effort on Tor (113 % vs 90 %): more hashing per counted share, i.e. more shares landing as uncles or too late for the PPLNS window.
  3. Confidence: moderate, not precise. With 3 blocks per arm the ~11 % effect sits only modestly above the clearnet block-to-block noise floor (±8–10 %), and per-block yield-eff ranges overlap (Tor 0.86–1.07 vs clearnet 0.97–1.17). Read it as "roughly 10 %", economically real and directionally consistent, but not a tight point estimate. A longer run would narrow the interval.

Recommendation — keep Tor the default (#165/#166 ship as-is in develop). Per the decision rule this is the "materially below clearnet" case, so the trade-off is documented rather than hidden. Not exposing the operator's home IP to the P2Pool network is the project's core value, and ~10 % is a modest, opt-out-able price for it. Operators who prioritise yield over IP privacy can set p2pool.clearnet: true (now functional; it was a no-op until the #294 firewall-toggle fix found during this benchmark) and accept the exposure. No per-sidechain split is warranted: the mechanism is the ~10 s share interval, common to every sidechain, so mini is representative. The absolute cost is largest on the smaller, faster chains where our share, and thus uncle sensitivity, is highest. This conclusion is mirrored operator-facing in ../privacy.md.