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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ config.toml
.cursor/
.windsurf/
.github/copilot-instructions.md
*.sqlite
*.sqlite
sv2-authority.key
50 changes: 50 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,56 @@ everything else bumps the **patch** version.

## [Unreleased]

### Added
- **SV2 pool identity (authority key pinning).** The Noise authority key now
persists across restarts (`[sv2] authority_key_file`, created on first start
with owner-only permissions, same pattern as bitcoind's `.cookie`), so the
pool keeps a stable identity that miners can pin. The base58check public key
(SRI `key-utils` format) is logged at startup and shown in the dashboard's
Connect modal with a copy button, and returned by `GET /api/info` as
`sv2_authority_pubkey`. Set it as the pool/authority public key on an SV2
miner to cryptographically verify the pool; miners that do not pin connect
exactly as before. `[sv2] persist_authority_key = false` opts out (fresh key
per process, the previous behavior).
- `[sv2] cert_validity_secs` — validity window of the per-connection
certificate (default one year). Short values are useful for testing how a
verifying miner handles certificate expiry and clock skew.
- Tests: full Noise handshakes against a pinning SRI initiator (correct key
accepted, wrong authority key rejected, expired certificate rejected, no-pin
still connects) plus authority-key-file round-trip/permission checks.

### Changed
- **Upgrade note (breaking for read-only deployments):** with SV2 enabled the
pool now creates `sv2-authority.key` (relative to its working directory) on
first start and **fails at boot if it cannot**. Deployments with a read-only
working directory, such as the shipped systemd unit with
`ProtectSystem=strict`, must set `[sv2] authority_key_file` to a writable
path (e.g. `/var/lib/solo-pool-rs/sv2-authority.key`) or set
`persist_authority_key = false`. Docker users who want the pool identity to
survive container re-creates should point it into the data volume
(`authority_key_file = "data/sv2-authority.key"`).

### Fixed
- Dashboard: the Connect modal **Copy buttons now actually copy** when the
dashboard is served over plain HTTP (the usual LAN case).
`navigator.clipboard` only exists in secure contexts, so the old code
selected the text and showed "Copied" without copying. Insecure contexts now
fall back to `document.execCommand('copy')`, and if even that fails the
button says "Copy manually" and leaves the text selected.
- **Duplicate-share tracking** no longer misreports or permits bounded replay:
a share is recorded for dedup only after it validates (invalid submissions
previously occupied slots, so a later identical valid submit was wrongly
rejected as `duplicate`), and the per-session set is cleared on every
clean-job broadcast, scoping replay protection to live jobs instead of FIFO
eviction (evict-then-resubmit could inflate share/hashrate stats).
- Pool **best-share / best-hashrate writes are monotonic end to end**: the
SQLite `UPDATE`s now carry a `?1 > ...` guard (matching the per-worker
variant) and the in-memory best-hashrate update is a CAS loop, so racing
writers can no longer regress a recorded best value.
- A block accepted by the **background submit retrier** (inline attempts
failed, e.g. while bitcoind restarts) now updates the dashboard block count
and last-block panel, not just the Prometheus counters.

## [0.5.1] - 2026-06-15

### Added
Expand Down
62 changes: 58 additions & 4 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ charming = "0.6"
codec_sv2 = { version = "5.0.0", features = ["noise_sv2"] }
noise_sv2 = "1.4.2"
secp256k1 = { version = "0.28", features = ["rand", "std"] }
key-utils = "1.2.0"

[dev-dependencies]
tokio-test = "0.4"
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,12 +263,14 @@ On a NerdQAxe++ (AxeOS ≥ v1.0.37):
| Field | Value |
|---|---|
| Stratum | select **Stratum V2** |
| Encryption | **on** (Noise), no authority pubkey needed; leave it unset |
| Encryption | **on** (Noise); authority pubkey optional, see below |
| Host / Port | `<your-server-ip>` : `3333` (same as SV1) |
| Worker | anything (used as the SV2 `user_identity`) |

The connection is secured with the SV2 **Noise** handshake (pool = responder); the device then opens an **Extended Channel** and is served `NewExtendedMiningJob` + `SetNewPrevHash` from the same `getblocktemplate` pipeline as SV1. Set `enabled = false` under `[sv2]` to refuse SV2 and serve SV1 only.

**Pool identity (optional pinning).** The pool signs each connection's Noise certificate with a persistent authority key and prints the base58check public key at startup (also shown in the dashboard's Connect modal, and at `GET /api/info`). Miners that support it can pin this key to cryptographically verify they are talking to your pool; miners that leave it unset connect exactly the same, encrypted but without identity verification. The key file (`[sv2] authority_key_file`, default `sv2-authority.key`) is created on first start; `persist_authority_key = false` reverts to a fresh key per process. Both the accept and reject paths are covered by tests that run a real handshake against a pinning SRI initiator, including wrong-key and expired-certificate cases.

---

## Dashboard & metrics
Expand Down
34 changes: 20 additions & 14 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,20 @@ underflow panic). Line references are as of that review and may drift.
funnel through a dedicated writer thread, enable WAL + `synchronous=NORMAL`),
and the dashboard `/history` + `/chart` SQLite scans (contend with the share
path on the same connection mutex; wrap in `spawn_blocking`).
- [ ] **Harden the duplicate-share set.** 4096-entry FIFO allows bounded replay
(evict-then-resubmit inflates share/hashrate stats); shares are also inserted
*before* validation, so invalid shares occupy slots and later identical
submits are misreported as `duplicate`. Scope dedup to live jobs and insert
only after validation passes. (`src/mining/validator.rs`)
- [ ] **Credit background-retrier block acceptance to dashboard stats.** A block
accepted by the PR #6 background retrier updates Prometheus counters but not
the dashboard block list / pool stats (needs session context plumbing).
- [x] **Harden the duplicate-share set** (unreleased, headed for v0.6.0):
shares are recorded for dedup only after validation passes, and the
per-session set clears on every clean-job broadcast (live-jobs scoping); the
4096 FIFO cap remains as a memory backstop only.
- [x] **Credit background-retrier block acceptance to dashboard stats**
(unreleased, headed for v0.6.0): worker + `PoolStats` are threaded through
`submit_found_block` into the resubmit task; retry success now mirrors the
inline-success stats update.

## Low

- [ ] Monotonic guard on pool best-share/best-hashrate SQLite `UPDATE`s
(`WHERE ?1 > ...`), matching the per-worker variant; also make the
best-hashrate in-memory update a CAS. (`src/stats.rs`)
- [x] Monotonic guard on pool best-share/best-hashrate SQLite `UPDATE`s
(`WHERE ?1 > ...`), matching the per-worker variant; best-hashrate in-memory
update is now a CAS. (unreleased, headed for v0.6.0)
- [x] Fix ghost-online accounting: repeated `mining.authorize` increments
`active_sessions` per call but disconnect decrements once, for the last name
only. (Fixed alongside the authorization cap: same-name re-auth is a no-op,
Expand All @@ -51,9 +51,15 @@ underflow panic). Line references are as of that review and may drift.
## Planned features

- [x] **v0.4.0: non-root Docker image** (shipped in v0.4.0, 2026-06-11).
- [ ] **SV2 identity pinning:** optional persistent Noise authority keypair via
config instead of the per-process ephemeral key (deferred in the
`protocol/sv2/noise.rs` docstring; today no miner verifies pool identity).
- [x] **SV2 identity pinning** (unreleased, headed for v0.6.0): persistent
Noise authority key (`[sv2] authority_key_file`, cookie-style
create-on-first-start), pubkey logged at boot + shown in the dashboard
Connect modal + `GET /api/info`; `persist_authority_key = false` opts out,
`cert_validity_secs` configurable. Verified on a NerdQAxe++: pinned key
verifies and mines, wrong key rejected. Note: the bitaxe/nerdqaxe firmware
checks only the Schnorr signature, never the validity window (no wall
clock); upstream enforcement-toggle PRs: bitaxeorg/ESP-Miner#1796,
shufps/ESP-Miner-NerdQAxePlus#656.
- [ ] **SV1-over-TLS (`stratum+ssl://`) — DEFERRED, build only on request.**
Decision (2026-06-15): not building it. The target audience is the
self-hosted *solo* crowd on a trusted LAN, where the value is marginal — solo
Expand Down
23 changes: 20 additions & 3 deletions config.toml.example
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,29 @@ found_block_dir = "found-blocks"
# NerdQAxe++ on AxeOS >= v1.0.37, "Stratum V2" selected) at the same host:port
# as your SV1 miners.
#
# The Noise authority keypair is generated automatically per process; the miner
# does not verify pool identity, so no key configuration is required.
#
# Set to false to refuse SV2 and serve SV1 only.
enabled = true

# Persist the Noise authority key so the pool keeps the same identity across
# restarts. The base58check public key is logged at startup and shown in the
# dashboard's Connect modal; set it as the pool/authority public key on the
# miner to cryptographically verify the pool (identity pinning). Miners that
# do not pin connect exactly as before. Set to false for a fresh key each
# start (pinning miners will then refuse to connect after every restart).
persist_authority_key = true

# Where the authority secret key lives (one base58check line, created with
# owner-only permissions on first start). Relative paths resolve against the
# service working directory, like stats_db_path. Supports ~ expansion.
authority_key_file = "sv2-authority.key"

# Validity window (seconds) of the certificate signed for each connection:
# valid_from = now, not_valid_after = now + cert_validity_secs. Miners that
# verify pool identity check this window against their own clock (SRI-based
# verifiers allow 10 s of drift), so short values expose device clock skew.
# Default: one year.
cert_validity_secs = 31536000

[bitcoin_rpc]
# Bitcoin RPC endpoint (core/knots)
url = "http://127.0.0.1:8332"
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ services:
# mkdir -p data && sudo chown -R 10001:10001 data
# Then in config.toml set stats_db_path = "data/pool_stats.sqlite" and
# found_block_dir = "data/found-blocks" so both land in this volume.
# The SV2 authority key defaults to /app/sv2-authority.key, which does not
# survive a container re-create. If miners pin the pool identity, also set
# authority_key_file = "data/sv2-authority.key" so the key lives here too.
- ./data:/app/data

# Without host networking, drop `network_mode: host`, add:
Expand Down
6 changes: 6 additions & 0 deletions packaging/systemd/solo-pool-rs.service
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ Group=solo-pool

# systemd creates and owns /var/lib/solo-pool-rs for you. Point the SQLite
# stats DB there: stats_db_path = "/var/lib/solo-pool-rs/pool_stats.sqlite"
# The SV2 authority key needs a writable path too. Its default
# ("sv2-authority.key") resolves against the working directory, which is
# read-only under ProtectSystem=strict below, and the pool fails at boot if it
# cannot create the key. In config.toml set:
# authority_key_file = "/var/lib/solo-pool-rs/sv2-authority.key"
# (or set persist_authority_key = false to keep an ephemeral per-process key).
StateDirectory=solo-pool-rs

# Logs go to the journal (journalctl -u solo-pool-rs). Keep log_dir empty in
Expand Down
Loading