|
| 1 | +# Final review — three-PR stack (PRs #6, #7, #8) |
| 2 | + |
| 3 | +**Date:** 2026-05-02 |
| 4 | +**Branch under review:** `feat/web-and-docs` (tip: `021ffa1`) |
| 5 | +**Stack:** PR #6 → #7 → #8, all draft, stacked off `main`. |
| 6 | +**Tests:** 242 passing, ~17s wall, no flakes. |
| 7 | +**Reviewers:** code, security, docs/UX, test coverage (4 parallel agents). |
| 8 | + |
| 9 | +Detailed reports: |
| 10 | +- [`review-code.md`](./review-code.md) |
| 11 | +- [`review-security.md`](./review-security.md) |
| 12 | +- [`review-docs.md`](./review-docs.md) |
| 13 | +- [`review-tests.md`](./review-tests.md) |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Verdict |
| 18 | + |
| 19 | +**Mergeable after one BLOCKER fix and four small SHOULD-FIX items.** |
| 20 | +Stack is well-structured, stdlib-only, hot path untouched, observers cleanly |
| 21 | +decoupled, bilingual docs are real (not machine-translated), zero HIGH security |
| 22 | +findings, 242 tests passing. |
| 23 | + |
| 24 | +The one blocking bug is in `solo.py` prev-hash byte-order — masked by the |
| 25 | +all-zero `FAKE_TEMPLATE` prevhash in `test_solo.py`, so against a real |
| 26 | +`bitcoind` solo mode would silently mine invalid blocks. |
| 27 | + |
| 28 | +## Blocker (fix before merge) |
| 29 | + |
| 30 | +### B1 — `solo.py:514-521` prev-hash word-swap is a no-op |
| 31 | + |
| 32 | +`_template_to_job` builds `prev_stratum_hex` such that `swap_words` in |
| 33 | +`_build_header_base` cancels out, leaving the mined header with `prev_be` |
| 34 | +display order instead of internal little-endian `prev_be[::-1]`. Submit-time |
| 35 | +serializer in `_assemble_header` independently does the right reversal, so |
| 36 | +hashing-time and submit-time headers disagree. |
| 37 | + |
| 38 | +**Hidden by:** `FAKE_TEMPLATE.previousblockhash = "0" * 64` (fixed point under |
| 39 | +any byte permutation). No real-bitcoind integration test catches it. |
| 40 | + |
| 41 | +**Fix sketch** (from code-reviewer): |
| 42 | +```python |
| 43 | +internal = bytes.fromhex(tmpl["previousblockhash"])[::-1] |
| 44 | +prev_stratum_hex = b"".join(internal[i:i+4][::-1] |
| 45 | + for i in range(0, 32, 4)).hex() |
| 46 | +``` |
| 47 | + |
| 48 | +**Test gap to close together:** add a `test_solo.py` case with a real |
| 49 | +non-symmetric mainnet block hash and assert `_build_header_base` produces |
| 50 | +exactly `prev_display[::-1]`. |
| 51 | + |
| 52 | +## Should-fix (recommend before merge, all small) |
| 53 | + |
| 54 | +### S1 — `docker-compose.yml:10` comment misnames web-dashboard port |
| 55 | +Comment points at `:8000` (which is `/metrics`+`/healthz`); web dashboard is |
| 56 | +on `:8001`. First-run users hit the Prometheus exposition page. (docs review) |
| 57 | + |
| 58 | +### S2 — `docker run` example in `deploy.{en,ru}.md` §6 has confusing positional |
| 59 | +The literal `docker` is the worker_name positional, but reads as a subcommand. |
| 60 | +Rename to `mybox` and add a comment. (docs review) |
| 61 | + |
| 62 | +### S3 — `CHANGELOG.md` link footer stale |
| 63 | +v0.3.0–v0.7.0 are unlinked; `[Unreleased]` still compares against v0.2.0. |
| 64 | +Markdown silently broken. (docs review) |
| 65 | + |
| 66 | +### S4 — README advanced-flags table omits `--log-file` |
| 67 | +Flag is real (`cli.py:108`), used in TUI workflow. Missing in EN and RU |
| 68 | +halves. (docs review) |
| 69 | + |
| 70 | +## Security MEDIUMs (not blocking the documented use-case, fix before any non-localhost deploy) |
| 71 | + |
| 72 | +### M-1 — `/api/events` SSE has no concurrent-connection cap |
| 73 | +Per-connection `queue.Queue(maxsize=256)` + thread; `ThreadingHTTPServer` |
| 74 | +spawns one thread per request; loopback default mitigates, but |
| 75 | +`docker-compose.yml` flips bind to `0.0.0.0`. **Fix:** `max_subscribers` cap |
| 76 | ++ HTTP 503 on overflow. |
| 77 | + |
| 78 | +### M-2 — `BitcoinRPC` cookie file read is unbounded and unguarded |
| 79 | +`Path(cookie_path).read_text()` with no size cap and no `try/except`. |
| 80 | +Footgun: `--rpc-cookie /tmp` OOMs the miner. **Fix:** stat → size limit 4 KiB |
| 81 | +→ `OSError` → friendly CLI error. |
| 82 | + |
| 83 | +### M-3 — ctypes loads `libcrypto-3.dll` by bare name on Windows |
| 84 | +DLL search order includes app dir → writable cwd hijack possible. **Fix:** |
| 85 | +`ctypes.WinDLL(..., winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)` or |
| 86 | +`os.add_dll_directory()` with a known-good path. |
| 87 | + |
| 88 | +### M-4 — Docker compose binds to `0.0.0.0` + ships Grafana `admin/admin` |
| 89 | +`ports: "8001:8001"` and `"3000:3000"` publish broadly; Docker bypasses |
| 90 | +host firewall via `iptables`. **Fix:** prefix with `127.0.0.1:`; make |
| 91 | +`GRAFANA_PASSWORD` mandatory via `${GRAFANA_PASSWORD:?...}` idiom. |
| 92 | + |
| 93 | +## Test gaps (no blockers, but high-value to add next iteration) |
| 94 | + |
| 95 | +1. **Mid-state ↔ ctypes parity sentinel** — guards against silent endianness |
| 96 | + regression in `_worker_ctypes` vs `_worker_hashlib_midstate`. Hardware- |
| 97 | + independent, cheap. |
| 98 | +2. **`SoloClient.reader_loop` RPC failure path** — uncovered. |
| 99 | +3. **`SoloClient` without `default_witness_commitment`** (regtest/non-segwit |
| 100 | + templates). |
| 101 | +4. **`cli._build_pool_list` and `_resolve_sha_backend`** — pure helpers, zero |
| 102 | + tests today. Add `tests/test_cli_helpers.py`. |
| 103 | +5. **`webui._serve_events` cleanup on subscriber drop** — verify |
| 104 | + `_subscribers` list returns to baseline length after disconnect. |
| 105 | +6. **`StatsProvider.publish_event` under concurrent subscribe/unsubscribe** — |
| 106 | + intentional design (snapshot-then-call) deserves a smoke test. |
| 107 | + |
| 108 | +## Non-blocking code concerns |
| 109 | + |
| 110 | +- `cli.py:460-466` — `stats_provider.update_hashrate` monkey-patch. Lift |
| 111 | + `last_hashrate_ts` into `StatsSnapshot` and drop the wrapper. Already flagged |
| 112 | + in PR-A handoff as an open question. |
| 113 | +- `notifier.py:307` — dead boolean clause; intent is probably |
| 114 | + `if item is None or self._stop_event.is_set():`. |
| 115 | +- `solo.py:489-499` — `deadbeef` extranonce marker has 2⁻³² collision risk. |
| 116 | + Use 16-byte `os.urandom`. |
| 117 | +- `solo.py:407-452` — `submit()` runs synchronously in mine thread; OK for |
| 118 | + learning code, document in `architecture.{en,ru}.md`. |
| 119 | +- `webui.py:331-336` — `repr(exc)` leaks into healthz HTTP body; loopback |
| 120 | + default mitigates. |
| 121 | +- `parallel.py:275-276` — pre-existing bare `except Exception: pass` on queue |
| 122 | + cleanup. Out of stack scope. |
| 123 | + |
| 124 | +## Docs nice-to-haves |
| 125 | + |
| 126 | +- N1: Validate `--solo` argument tuple before BTC address validation. |
| 127 | +- N2: argparse help text is Russian-only; bilingual everywhere else. |
| 128 | +- N3: `architecture.{en,ru}.md` doesn't note that telegram inbound requires |
| 129 | + `HOPE_HASH_TELEGRAM_INBOUND=1` opt-in. |
| 130 | +- N6/N7: `deploy.{en,ru}.md` §3 incorrectly says "503 otherwise" — actual |
| 131 | + is 200 for `degraded`, 503 only for `down`. |
| 132 | +- N10: `Dockerfile` `EXPOSE` lists 8000+9090 but compose uses 8000+8001. |
| 133 | +- N11: `architecture.{en,ru}.md` file-map omits `_logging.py`/`__main__.py`. |
| 134 | + |
| 135 | +## Praise (worth keeping in mind for future agents) |
| 136 | + |
| 137 | +- **`StatsProvider` as canonical bus** — clean decoupling of mine() from TUI, |
| 138 | + web, healthz, Prometheus. Pub/sub added in PR C without touching call sites. |
| 139 | +- **`PoolList`** — `mark_failed` returns rotation flag, `full_cycle_failed` |
| 140 | + distinguishes flap from outage, `RLock` prevents the obvious deadlock and |
| 141 | + there's a test pinning the invariant. |
| 142 | +- **ctypes loader hygiene** — `c_void_p` restypes (catches 64-bit truncation), |
| 143 | + `EVP_sha256` symbol probe (catches wrong DLL), `try/finally` around |
| 144 | + `EVP_MD_CTX_free` (no leak/UAF). |
| 145 | +- **`render_html` is fully static** — no user input echoed in, no CDN, no |
| 146 | + external scripts. XSS-safe by construction. |
| 147 | +- **`test_notifier_timing.py`** — exemplary regression sentinel for the |
| 148 | + submit-vs-ack semantic that already bit the project once. |
| 149 | +- **`test_solo.py` (38 tests)** — well-spent on the most error-prone module; |
| 150 | + byte-level assertions on `_varint`, `_push_data`, `_serialize_height`, |
| 151 | + witness commitment parse/compute. |
| 152 | +- **Bilingual docs** — EN/RU mirror line-for-line; Russian reads as |
| 153 | + written-by-a-human, technical terms stay English by convention. |
| 154 | +- **Defaults are conservative** — `--web-host 127.0.0.1`, `--web-port 0`, |
| 155 | + `HOPE_HASH_TELEGRAM_INBOUND=0` — exactly the right posture. |
| 156 | + |
| 157 | +## Recommended merge order |
| 158 | + |
| 159 | +1. Land **B1** fix + the matching prev-hash test against PR #7 branch. |
| 160 | +2. (Optional now / can defer) land **S1–S4** doc/comment fixes against PR #8 |
| 161 | + branch. |
| 162 | +3. (Defer to a follow-up PR) **M-1–M-4** security MEDIUMs and the six |
| 163 | + test-coverage additions. |
| 164 | +4. Merge PR #6 → main, then rebase #7 onto main + merge, then rebase #8 onto |
| 165 | + main + merge. |
| 166 | + |
| 167 | +Total fix budget for blocking + should-fix: ~30 minutes of engineering work |
| 168 | +plus tests. The four MEDIUMs and the test gaps are a clean follow-up PR. |
0 commit comments